Tento dokument je sbírkou cvičení a komentovaných příkladů
zdrojového kódu. Každá kapitola odpovídá jednomu týdnu semestru a
tedy jednomu cvičení. Cvičení v druhém týdnu semestru („nulté“) je
určeno k seznámení se s výukovým prostředím, studijními materiály a
základními nástroji ekosystému.
Každá část sbírky (zejména tedy všechny ukázky a příklady) jsou také
k dispozici jako samostatné soubory, které můžete upravovat a
spouštět. Této rozdělené verzi sbírky říkáme zdrojový balík.
Aktuální verzi1 (ve všech variantách) můžete získat dvěma způsoby:
Ve studijních materiálech předmětu v ISu – soubory PDF ve
složce text, zdrojový balík ve složkách 00 (organizační
informace), 01 až 12 (jednotlivé kapitoly = týdny semestru),
dále s1 až s3 (sady úloh) a konečně ve složce sol vzorová
řešení. Doporučujeme soubory stahovat dávkově pomocí volby
„stáhnout jako ZIP“.
Po přihlášení na studentský server aisa (buď za pomoci ssh
nebo putty) zadáním příkazu ib111 update. Všechny výše
uvedené složky pak naleznete ve složce ~/ib111.
Tato kapitola (složka) dále obsahuje závazná pravidla a
organizační pokyny. Než budete pokračovat, pozorně si je prosím
přečtěte.
Pro komunikaci s organizátory kurzu slouží diskusní fórum v ISu
(více informací naleznete v části T.1). Nepište prosím organizátorům
ani cvičícím maily ohledně předmětu, nejste-li k tomu specificky
vyzváni. S žádostmi o výjimky ze studijních povinností, omluvenkami,
atp., se obracejte vždy na studijní oddělení.
Než začnete pracovat na přípravách nebo příkladech ze sady, vždy se prosím ujistěte, že máte jejich aktuální verzi. Zadání příprav lze považovat za finální počínaje půlnocí na pondělí odpovídajícího týdne, sady podobně půlnocí na první pondělí odpovídajícího bloku. Bude-li nutné provést nějaké změny v zadání později, budete o nich informováni v diskusním fóru.
Tento předmět sestává z cvičení, sad domácích úloh a závěrečného
testu (zkoušky). Protože se jedná o „programovací“ předmět, většina
práce v předmětu – a tedy i jeho hodnocení – se bude zaměřovat na
praktické programování. Je důležité, abyste programovali co možná
nejvíce, ideálně každý den, ale minimálně několikrát každý týden.
K tomu Vám budou sloužit příklady v této sbírce a domácí úlohy,
kterých budou za semestr 3 sady, a budou znatelně většího rozsahu
(maximálně malé stovky řádků). V obou případech bude v průběhu
semestru stoupat náročnost – je tedy důležité, abyste drželi krok a
práci neodkládali na poslední chvíli.
Protože programování je těžké, bude i tento kurz těžký – je zcela
nezbytné vložit do něj odpovídající úsilí. Doufáme, že kurz úspěšně
absolvujete, a co je důležitější, že se v něm toho naučíte co
nejvíce. Je ale nutno podotknout, že i přes svou náročnost je tento
kurz jen malým krokem na dlouhé cestě.
Předmět je rozdělen do 4 bloků (čtvrtý blok patří do zkouškového
období). Do každého bloku v semestru patří 4 kapitoly (témata) a
jim odpovídající 4 cvičení.
V následujících sekcích naleznete detailnější informace a závazná
pravidla kurzu: doporučujeme Vám, abyste se s nimi důkladně
seznámili. Zbytek sbírky je pak rozdělen na části, které odpovídají
jednotlivým týdnům semestru. Důležité: během druhého týdne
semestru už budete řešit přípravy z první kapitoly, přestože první
cvičení je ve až v týdnu třetím. Nulté cvičení je volitelné a není
nijak hodnoceno.
Kapitoly jsou číslovány podle témat z předchozí tabulky: ve třetím
týdnu semestru se tedy ve cvičení budeme zabývat tématy, ke kterým
jste v druhém týdnu vypracovali a odevzdali přípravy.
Tento kurz vyžaduje značnou aktivitu během semestru. V této sekci
naleznete přehled důležitých událostí formou kalendáře. Jednotlivé
události jsou značeny takto (bližší informace ke každé naleznete
v následujících odstavcích tohoto úvodu):
„#X“ – číslo týdne v semestru,
„cv0“ – tento týden běží „nulté“ cvičení (kapitola B),
„cv1“ – tento týden probíhají cvičení ke kapitole 1,
„X/v“ – mezivýsledek verity testů příprav ke kapitole X,
„X/p“ – poslední termín odevzdání příprav ke kapitole X,
„sX/Y“ – Yté kolo verity testů k sadě X.
Nejdůležitější události jsou zvýrazněny: termíny odevzdání příprav a
poslední termín odevzdání úloh ze sad (obojí vždy o 23:59 uvedeného
dne).
Abyste předmět úspěšně ukončili, musíte v každém bloku2 získat 50
bodů. Žádné další požadavky nemáme.
Výsledná známka závisí na celkovém součtu bodů (splníte-li
potřebných 4×50 bodů, automaticky získáte známku alespoň E). Hodnota
ve sloupci „předběžné minimum“ danou známku zaručuje – na konci
semestru se hranice ještě mohou posunout směrem dolů tak, aby
výsledná stupnice přibližně odpovídala očekávané distribuci dle
ECTS.3
známka
předběžné minimum
po vyhodnocení semestru
A
360
90. percentil + 75
B
320
65. percentil + 75
C
280
35. percentil + 75
D
240
10. percentil + 75
E
200
200
Body lze získat mnoha různými způsoby (přesnější podmínky naleznete
v následujících sekcích této kapitoly). V blocích 1-3 (probíhají
během semestru) jsou to:
za každou úspěšně odevzdanou přípravu 1 bod (max. 6 bodů každý
týden, nebo 24/blok),
za každou přípravu, která projde „verity“ testy navíc další 1 bod
(max. 6 bodů každý týden, nebo 24/blok),
za účast4 na cvičení získáte 3 body (max. tedy 12/blok),
za aktivitu ve cvičení 3 body (max. tedy 12/blok).
Za přípravy a cvičení lze tedy získat teoretické maximum 72 bodů.
Dále můžete získat:
7 bodů za úspěšně vyřešený příklad ze sady domácích úloh
(maximálně 4 příklady, celkem tedy až 28/blok).
Konečně blok 4, který patří do zkouškového období, nemá ani cvičení
ani sadu domácích úloh. Body získáte účastí na závěrečném testu:
16 bodů za každý zkouškový příklad (5 příkladů, maximálně tedy
celkem 80/blok),
-2 až +2 body za každou z 10 teoretických otázek (celkem až
20/blok).
Percentil budeme počítat z bodů v semestru (první tři bloky) a bude brát do úvahy všechny studenty, bez ohledu na ukončení, kteří splnili tyto tři bloky (tzn. mají potřebné minimum 3×50 bodů).
V případě, že jste řádně omluveni v ISu, nebo Vaše cvičení odpadlo (např. padlo na státní svátek), můžete body za účast získat buď náhradou v jiné skupině (pro státní svátky dostanete instrukce mailem, individuální případy si domluvte s cvičícími obou dotčených skupin). Nemůžete-li účast nahradit takto, domluvte se se svým cvičícím (v tomto případě lze i mailem) na vypracování 3 rozšířených příkladů ze sbírky (přesné detaily Vám sdělí cvičící podle konkrétní situace). Neomluvenou neúčast lze nahrazovat pouze v jiné skupině a to nejvýše jednou za semestr.
Jak již bylo zmíněno, chcete-li se naučit programovat, musíte
programování věnovat nemalé množství času, a navíc musí být tento
čas rozložen do delších období – semestr nelze v žádném případě
doběhnout tím, že budete týden programovat 12 hodin denně, i když to
možná pokryje potřebný počet hodin. Proto od Vás budeme chtít,
abyste každý týden odevzdali několik vyřešených příkladů z této
sbírky. Tento požadavek má ještě jeden důvod: chceme, abyste vždy
v době cvičení už měli látku každý samostatně nastudovanou, abychom
mohli řešit zajímavé problémy, nikoliv opakovat základní pojmy.
Také Vás prosíme, abyste příklady, které plánujete odevzdat, řešili
vždy samostatně: případnou zakázanou spolupráci budeme trestat (viz
také konec této kapitoly).
Každý příklad obsahuje základní sadu testů. To, že Vám tyto testy
prochází, je jediné kritérium pro zisk základních bodů za odevzdání
příprav. Poté, co příklady odevzdáte, budou tytéž testy na Vašem
řešení automaticky spuštěny, a jejich výsledek Vám bude zapsán do
poznámkového bloku. Smyslem tohoto opatření je zamezit případům, kdy
omylem odevzdáte nesprávné, nebo jinak nevyhovující řešení, aniž
byste o tom věděli. Velmi silně Vám proto doporučujeme odevzdávat
s určitým předstihem, abyste případné nesrovnalosti měli ještě čas
vyřešit. Krom základních („sanity“) testů pak ve čtvrtek o 23:59 a
znovu v sobotu o 23:59 (těsně po konci odevzdávání) spustíme
rozšířenou sadu testů („verity“).
Za každý odevzdaný příklad, který splnil základní („sanity“) testy
získáváte jeden bod. Za příklad, který navíc splnil rozšířené
testy získáte další bod (tzn. celkem 2 body). Výsledky testů
naleznete v poznámkovém bloku v informačním systému.
Příklady můžete odevzdávat:
do odevzdávárny s názvem NN v ISu (např. 01),
příkazem ib111 submit ve složce ~/ib111/NN.
Podrobnější instrukce naleznete v kapitole T (technické informace,
soubory 00/t*).
Termíny pro odevzdání příprav k jednotlivým kapitolám jsou shrnuty
v přehledovém kalendáři v části A.1 takto:
„01/v“ – předběžné (čtvrteční) verity testy pro příklady z první
kapitoly,
„01/p“ – poslední (sobotní) termín odevzdání příprav z 1.
kapitoly,
Těžiště tohoto předmětu je jednoznačně v samostatné domácí práci –
učit se programovat znamená zejména hodně programovat. Společná
cvičení sice nemohou tuto práci nahradit, mohou Vám ale přesto
v lecčem pomoct. Smyslem cvičení je:
analyzovat problémy, na které jste při samostatné domácí práci
narazili, a zejména prodiskutovat, jak je vyřešit,
řešit programátorské problémy společně (s cvičícím, ve dvojici,
ve skupině) – nahlédnout, jak o programech a programování uvažují
ostatní, a užitečné prvky si osvojit.
Cvičení je rozděleno na dva podobně dlouhé segmenty, které
odpovídají těmto bodům. První část probíhá přibližně takto:
cvičící vybere ty z Vámi odevzdaných příprav, které se mu zdají
něčím zajímavé – ať už v pozitivním, nebo negativním smyslu,
řešení bude anonymně promítat na plátno a u každého otevře
diskusi o tom, čím je zajímavé;
Vaším úkolem je aktivně se do této diskuse zapojit (můžete se
například ptát, proč je daná věc dobře nebo špatně, a jak by se
udělala lépe, vyjádřit svůj názor, odpovídat na dotazy
cvičícího),
k promítnutému řešení se můžete přihlásit a ostatním přiblížit,
proč je napsané tak, jak je, nebo klidně i rozporovat případnou
kritiku (není to ale vůbec nutné),
na Vaši žádost lze ve cvičení analogicky probrat neúšpěšná
řešení příkladů (a to jak příprav, tak příkladů z uzavřených
sad).
Druhá část cvičení je variabilnější, ale bude se vždy točit kolem
bodů za aktivitu (každý týden můžete za aktivitu získat maximálně 3
body).
Ve čtvrtém, osmém a dvanáctém týdnu proběhnou „vnitrosemestrálky“,
kde budete řešit samostatně dva příklady ze sbírky, bez možnosti
hledat na internetu – tak, jak to bude na závěrečném testu; každé
úspěšné řešení (tzn. takové, které splní verity testy) získá 3 body
za aktivitu pro daný týden (celkem tedy lze za příklady získat 6
bodů). Navíc dostanete 3 teoretické otázky, po jednom bodu, celkově
lze tedy během vnitrosemestrálky získat až 9 bodů (počítají se jako
aktivita, tzn. platí celkový limit 12/blok).
V ostatních týdnech budete ve druhém segmentu kombinovat různé
aktivity, které budou postavené na příkladech typu r z aktuální
kapitoly (které konkrétní příklady budete ve cvičení řešit, vybere
cvičící, může ale samozřejmě vzít v potaz Vaše preference):
Můžete se přihlásit k řešení příkladu na plátně, kdy primárně
vymýšlíte řešení Vy, ale zbytek třídy Vám bude podle potřeby
radit, nebo se ptát co/jak/proč se v řešení děje. U jednodušších
příkladů se od Vás bude také očekávat, že jako součást řešení
doplníte testy.
Cvičící Vám může zadat práci ve dvojicích – první dvojice, která
se dopracuje k funkčnímu řešení získá možnost své řešení
předvést zbytku třídy – vysvětlit jak a proč funguje, odpovědět
na případné dotazy, opravit chyby, které v řešení publikum
najde, atp. – a získat tak body za aktivitu. Získané 3 body
budou rozděleny rovným dílem mezi vítězné řešitele.
Příklad můžete také řešit společně jako skupina – takto
vymyšlený kód bude zapisovat cvičící (body za aktivitu se
v tomto případě neudělují).
Ke každému bloku patří sada 4–6 domácích úloh. Na úspěšné odevzdání
každé domácí úlohy budete mít 12 pokusů rozložených do 4 týdnů
odpovídajícího bloku cvičení. Odevzdávání bude otevřeno vždy v 0:00
prvního dne bloku (tzn. 24h před prvním spuštěním verity testů).
Termíny odevzdání (vyhodnocení verity testů) jsou vždy v pondělí,
středu a pátek v 23:59 – vyznačeno jako s1/1–12, s2/1–12 a s3/1–12
v přehledovém kalendáři v části A.1.
Součástí každého zadání je jeden zdrojový soubor (kostra), do
kterého své řešení vepíšete. Vypracované příklady lze pak odevzdávat
stejně jako přípravy:
do odevzdávárny s názvem sN_úkol v ISu (např. s1_a_queens),
příkazem ib111 submit sN_úkol ve složce ~/ib111/sN, např.
ib111 submit s1_a_queens.
Podrobnější instrukce naleznete opět v kapitole T.
Vyhodnocení Vašich řešení probíhá ve třech fázích, a s každou z nich
je spjata sada automatických testů. Tyto sady jsou:
„syntax“ – kontroluje, že odevzdaný program je syntakticky
správně, lze jej přeložit a prochází základními statickými
kontrolami,
„sanity“ – kontroluje, že odevzdaný program se chová „rozumně“ na
jednoduchých případech vstupu; tyto testy jsou rozsahem a stylem
podobné těm, které máte přiložené k příkladům ve cvičení,
„verity“ – důkladně kontrolují správnost řešení, včetně složitých
vstupů a okrajových případů.
Fáze na sebe navazují v tom smyslu, že nesplníte-li testy v některé
fázi, žádná další se už (pro dané odevzdání) nespustí. Pro splnění
domácí úlohy je klíčová fáze „verity“, za kterou jsou Vám uděleny
body. Časový plán vyhodnocení fází je následovný:
kontrola „syntax“ se provede obratem (do cca 5 minut od
odevzdání),
kontrola „sanity“ každých 6 hodin počínaje půlnocí (tzn. 0:00,
6:00, 12:00, 18:00),
kontrola „verity“ se provede v pondělí, středu a pátek ve 23:59
(dle tabulky uvedené výše).
Vyhodnoceno je vždy pouze nejnovější odevzdání, a každé odevzdání je
vyhodnoceno v každé fázi nejvýše jednou. Výsledky naleznete
v poznámkových blocích v ISu (každá úloha v samostatném bloku),
případně je získáte příkazem ib111 status.
Za každý domácí úkol, ve kterém Vaše odevzdání v příslušném termínu
splní testy „verity“, získáte 7 bodů (strop bodů za úkoly je 28 za
blok, počítají se tedy maximálně čtyři úspěšně vyřešené úkoly).
Příklady, které se Vám nepodaří vyřešit kompletně (tzn. tak, aby na
nich uspěla kontrola „verity“) nebudeme hodnotit. Nicméně může
nastat situace, kdy byste potřebovali na „téměř hotové“ řešení
zpětnou vazbu, např. proto, že se Vám nepodařilo zjistit, proč
nefunguje.
Taková řešení můžou být předmětem společné analýzy ve cvičení,
v podobném duchu jako probíhá rozprava kolem odevzdaných příprav
(samozřejmě až poté, co pro danou sadu skončí odevzdávání). Máte-li
zájem takto rozebrat své řešení, domluvte se, ideálně s předstihem,
se svým cvičícím. To, že jste autorem, zůstává mezi cvičícím a Vámi
– Vaši spolužáci to nemusí vědět (ke kódu se samozřejmě můžete
v rámci debaty přihlásit, uznáte-li to za vhodné). Stejná pravidla
platí také pro nedořešené přípravy (musíte je ale odevzdat).
Tento mechanismus je omezen prostorem ve cvičení – nemůžeme zaručit,
že v případě velkého zájmu dojde na všechny (v takovém případě
cvičící vybere ta řešení, která bude považovat za přínosnější pro
skupinu – je tedy možné, že i když se na Vaše konkrétní řešení
nedostane, budete ve cvičení analyzovat podobný problém v řešení
někoho jiného).
Zkouškové období tvoří pomyslný 4. blok a platí zde stejné kritérium
jako pro všechny ostatní bloky: musíte získat alespoň 50 bodů.
Závěrečný test:
proběhne v počítačové učebně bez přístupu k internetu nebo
vlastním materiálům,
k dispozici budete mít přehled jazyka (ib111.reference.pdf a
.html) a zabudovanou nápovědu dostupných programů (jiné
materiály nejsou povoleny),
budete moct používat textový editor, interpret jazyka Python a
vývojová prostředí Thonny a VS Code.
Na vypracování testu budete mít 4 hodiny čistého času, a bude
sestávat ze dvou částí (zadávají a odevzdávají se ovšem společně):
pět programovacích příkladů, které budou hodnoceny automatickými
testy; za každý příklad, který splní testy „verity“ získáte 16
bodů (za všechny ostatní 0), za celkem 0 až 80 bodů,
deset teoretických otázek, přitom každá bude složená z 5 tvrzení,
z toho dvou pravdivých a tří nepravdivých; hodnocení/otázka:
-2 body jsou-li všechny vybrané odpovědi nepravdivé,
-1 bod za 1 pravdivou + 1 nepravdivou, nebo za žádnou odpověď,
0 bodů za 1 pravdivou a druhou nevybranou,
2 body za 2 pravdivé odpovědi,
celkem za -20 až +20 bodů.
Celkový maximální zisk je tedy 100 bodů (80+20). Základní možnosti,
jak splnit minimální bodovou hranici, jsou 3 příklady + 2 body za
teorii, nebo 2 příklady + 18 bodů za teorii. Nechcete-li se teorií
vůbec zabývat, máte také možnost vyřešit 4 příklady (64 - 10 = 54
bodů).
Programovací příklady budou na stejné úrovni obtížnosti jako
příklady typu p/r/v ze sbírky.
Během zkoušky můžete kdykoliv odevzdat (na počet odevzdání není
žádný konkrétní limit) a vždy dostanete zpět výsledek testů syntaxe
a sanity. Součástí zadání bude navíc soubor tokens.txt, kde
naleznete 3 kódy. Každý z nich lze použít nejvýše jednou (vložením
do komentáře do jednoho z příkladů), a každé použití kódu odhalí
výsledek verity testu pro ten soubor, do kterého byl vložen. Toto se
projeví pouze při prvním odevzdání s vloženým kódem, v dalších
odevzdáních bude tento kód ignorován (bez ohledu na soubor, do
kterého bude vložen).
proběhne v rámci cvičení programovací test na 60 minut. Tyto testy
budou probíhat za stejných podmínek, jako výše popsaný závěrečný
test (slouží tedy mimo jiné jako příprava na něj). Řešit budete vždy
ale pouze dva příklady, přitom za každý můžete získat 3 body
(splní-li verity testy) a dále 3 teoretické otázky (hodnoceny jedním
bodem za dvě pravdivá tvrzení, jinak nulou). Celkem tak můžete
získat 0 až 9 bodů, které se počítají jako aktivita v příslušném
bloku. Součástí zadání bude také 1 token pro odhalení výsledku
verity testu.
Na všech zadaných problémech pracujte prosím zcela samostatně
(zejména tedy bez pomoci spolužáků, třetích stran, nebo jazykových
modelů) – toto se týká jak příkladů ze sbírky, které budete
odevzdávat, tak domácích úloh ze sad. To samozřejmě neznamená, že
Vám zakazujeme společně studovat a vzájemně si pomáhat látku
pochopit: k tomuto účelu můžete využít všechny zbývající příklady ve
sbírce (tedy ty, které nebude ani jeden z Vás odevzdávat), a
samozřejmě nepřeberné množství příkladů a cvičení, které jsou
k dispozici online.
Příklady, které odevzdáváte, slouží ke kontrole, že látce skutečně
rozumíte, a že dokážete nastudované principy prakticky aplikovat.
Tato kontrola je pro Váš pokrok naprosto klíčová – je velice snadné
získat pasivním studiem (čtením, posloucháním přednášek, studiem již
vypracovaných příkladů) pocit, že něčemu rozumíte. Dokud ale sami
nenapíšete na dané téma několik programů, jedná se pravděpodobně
skutečně pouze o pocit.
Abyste nebyli ve zbytečném pokušení kontroly obcházet, nedovolenou
spolupráci budeme relativně přísně trestat. Za každý prohřešek Vám
bude strženo v každé instanci (jeden týden příprav se počítá jako
jedna instance, příklady ze sad se počítají každý samostatně):
1/2 bodů získaných (ze všech příprav v dotčeném týdnu, nebo za
jednotlivý příklad ze sady) zaokrouhleno na celé body nahoru,
navíc 10 bodů z hodnocení bloku, do kterého opsaný příklad patří,
konečně 10 bodů (navíc k předchozím 10) z celkového hodnocení.
Opíšete-li tedy například 2 přípravy ve druhém týdnu a:
Váš celkový zisk za přípravy v tomto týdnu je 5 bodů,
Váš celkový zisk za první blok je 60 bodů,
jste automaticky hodnoceni známkou X (60 - 2,5 - 10 je méně než
potřebných 50 bodů). Podobně s příkladem z první sady (60 - 3,5 -
10), atd. Máte-li v bloku bodů dostatek (např. 80 - 5 - 10 ≥ 50), ve
studiu předmětu pokračujete, ale započte se Vám ještě navíc
penalizace 10 bodů do celkové známky. Přestává pro Vás proto platit
pravidlo, že 4 splněné bloky jsou automaticky E nebo lepší.
V situaci, kdy:
za bloky máte před penalizací 66, 52, 51, 54,
v prvním bloku jste opsali domácí úkol,
budete penalizováni:
v prvním bloku 10 + 4, tzn. bodové zisky za bloky budou efektivně
52, 52, 51, 54,
v celkovém hodnocení 10, tzn. celkový zisk 52 + 52 + 51 + 54 - 10
= 199, a budete tedy hodnoceni známkou F.
To, jestli jste příklad řešili společně, nebo jej někdo vyřešil
samostatně, a poté poskytl své řešení někomu dalšímu, není pro účely
kontroly opisování důležité. Všechny „verze“ řešení odvozené ze
společného základu (včetně situace, kdy je tento základ odpovědí
jazykového modelu) budou penalizovány stejně. Taktéž zveřejnění
řešení budeme chápat jako pokus o podvod, a budeme jej trestat, bez
ohledu na to, jestli někdo stejné řešení odevzdá, nebo nikoliv.
Podotýkáme ještě, že kontrola opisování nespadá do desetidenní
lhůty pro hodnocení průběžných kontrol. Budeme se sice snažit
opisování kontrolovat co nejdříve, ale odevzdáte-li opsaný příklad,
můžete být bodově penalizováni kdykoliv (tedy i dodatečně, a to až
do konce zkouškového období).
Tato kapitola je náplní cvičení ve druhém týdnu semestru, a jejím
smyslem je seznámit Vás s organizací cvičení, se studijními
materiály (tedy zejména touto sbírkou), s programovacím prostředím
Thonny a se základními elementy syntaxe jazyka Python. Zároveň Vám
připomeneme (nebo ukážeme) základy algoritmizace pomocí tzv. želví
grafiky.
V tomto kurzu budeme používat jazyk Python, resp. jeho značně
zjednodušenou podobu.5 V této úvodní kapitole budeme programy
zapisovat pouze na intuitivní úrovni: všechny konstrukce, které
potřebujete, můžete odvodit z příkladů v ukázkových zdrojových
kódech.
Každá další kapitola bude obsahovat sekci, která uvede syntaxi
(zápis) a sémantiku (význam, chování) nových jazykových prostředků.
Od chvíle, kdy bude nějaký nový prostředek takto uveden, jej můžete
ve svých programech využívat.6 Naopak, nic co nebylo tímto způsobem
uvedeno, pro účely tohoto kurzu neexistuje, i když to třeba
naleznete na internetu, nebo to znáte z předchozího programování
v jazyce Python.
Nicméně bude vždy platit, že programy, které v tomto kurzu naprogramujete, jsou plnohodnotné programy ve skutečném (neomezeném) jazyce Python. Nemusíte se tedy bát, že byste znalosti, které se tu naučíte, nevyužili v praxi.
V sadách domácích úloh se budou objevovat zadání, která využívají jazyk ze začátku bloku – i v případě, když takovou úlohu začnete řešit později, platí omezení jazyka na týden uvedený v záhlaví zadání.
Jednotlivé kapitoly sbírky obsahují 5 druhů příkladů: první sada
jsou tzv. ukázky – jedná se o komentované řešení nějakého
problému, které Vám ilustruje použití konstrukcí, které v daném
týdnu budeme ve cvičení potřebovat. Tyto ukázky nenahrazují
přednášku, přestože s ní mají určitý překryv – slouží k jejímu
doplnění delšími, komentovanými ukázkami použití, které můžete
využít jako inspiraci při řešení příkladů z ostatních částí. Tato
kapitola obsahuje pět ukázek:
square – kreslení čtverce přímo a pomocí cyklu
hexagon – použití podprogramu
boxes – podprogramy s parametry
isosceles – použití proměnné
flower – podmíněné provádění kódu
Jak ukázky, tak příklady v dalších sekcích, mohou být označeny
dýkou (†): jedná se o složitější příklady, které byste nicméně měli
být schopni řešit (i bez dodatečných znalostí). Příklady označené
dvojitou dýkou (‡) naopak předbíhají probranou látku, a neumíte-li
je vyřešit, není to žádný problém.
Další část obsahuje „elementární“ příklady, které by měly sloužit
k tomu, abyste si v rychlosti ověřili, že rozumíte základním
konstrukcím a pojmům představeným v přednášce a ukázkách.
Vypracovaná řešení této kategorie příkladů naleznete v kapitole R,
resp. ve složce sol ve studijních materiálech. Do této kapitoly
jsou zařazeny tyto elementární úlohy:
pentagon – pravidelný pětiúhelník
right – pravoúhlý trojúhelník (parametrický)
polygon – pravidelný n-úhelník
Další část tvoří přípravy: jsou to příklady, ze kterých si některé
vyberete a samostatně vyřešíte v předstihu před samotným cvičením
k danému tématu. Za tyto příklady dostáváte body, ale pouze pokud
odevzdáte funkční řešení nejpozději v sobotu před příslušným
cvičením.
Přípravy pro tento týden si můžete vyřešit dopředu také – je to ale
výjimečně bez bodů:
trapezoid – rovnoramenný lichoběžník
fence – plot pomocí cyklu
spiral – spirála
heartbeat – stylizované EKG pomocí cyklu
diamond – kreslení stylizovaného diamantu
tunnel – soustředné čtverce (pohled do „tunelu“)
Předposlední část každé kapitoly tvoří řešené (rozšířené) příklady
– tyto mají opět přiložená vzorová řešení. Část jich budete řešit ve
cvičeních, část můžete použít pro další domácí přípravu (s možností
samostatné kontroly svého řešení vůči tomu vzorovému) nebo také jako
zdroj příkladů k procvičení před zkouškou. Tento týden do této
kategorie spadají následující příklady:
circle – kružnice
pizza † – kruhová výseč
target – terč (soustředné kružnice)
arrow – obrys šipky
koch ‡ – Kochova vločka
hilbert ‡ – Hilbertova křivka
Kapitolu uzavírají příklady volitelné, které nejsou ve sbírce
vyřešené, ale na kterých si můžete látku dále procvičovat.
Smyslem první ukázky je předvést základní „příkazy“ (procedury –
tento pojem si přesněji vysvětlíme v dalších ukázkách) pro
kreslení obrázků. Tyto procedury ovládají „želvu“, která se
pohybuje po plátně a kreslí přitom čáru. Procedura forward želvě
poručí, aby se posunula o danou vzdálenost vpřed (a nakreslila
u toho úsečku ze své původní polohy do své nové polohy). Procedury
left a right nic nekreslí, pouze želvou otočí o daný úhel
(zadaný v stupních) doleva, resp. doprava.
Dovolíme-li želvě vracet se „po vlastních stopách“, stačí nám tyto
3 procedury na vykreslení libovolného spojitého obrazce. Pro
začátek zkusíme nakreslit čtverec:
def square():
Čtverec lze nakreslit jednoduše jako 4 navazující úsečky
stejné délky, přičemž každé dvě po sobě jdoucí svírají
pravý úhel.
Předchozí definice square nás ale příliš neuspokojuje: k čemu
máme počítač, když jsme museli každý krok explicitně popsat?
Zejména je na první pohled vidět, že příkazy se opakují. Jistě by
bylo dobré, abychom mohli počítači sdělit, že má nějakou akci
provést 4×, místo abychom ji zapsali 4× pod sebe – to je v
podstatě základní mechanismus, kterým nám počítač šetří práci.
def square_loop():
Základní formou tzv. cyklu (angl. loop) je příkaz „proveď
akci n krát“, který se v Pythonu zapisuje jako for i in
range(n) – v našem případě bude n = 4:
for i in range(4):
Následuje tzv. tělo cyklu, které je tvořeno (odsazeným)
seznamem příkazů, které se budou opakovat.
forward(100)
right(90)
Pozorný čtenář si jistě všiml, že definice square a
square_loop nejsou zcela ekvivalentní: ta druhá obsahuje
jedno použití procedury right navíc. Pro tuto chvíli je nám
to jedno, protože není-li volání right následováno žádným
použitím forward, nebude mít na výsledný obrázek dopad.
Nicméně obecně toto neplatí a je potřeba si na podobné
okrajové případy dávat pozor.
Následuje definice main, smyslem které je demonstrovat funkčnost
dříve definovaných square a square_loop.
def main(): # demo
Nejprve necháme želvu vykreslit čtverec „naivním“ způsobem,
bez použití cyklu (první z definic výše).
square()
Dále želvu požádáme, aby se přesunula na jiné místo plátna,
aniž by nakreslila čáru: tento kus kódu pro nás není příliš
podstatný, jeho smyslem je pouze vykreslit dva obrázky na jedno
plátno, abychom je mohli lehce srovnat.
penup()
setheading(0)
forward(200)
pendown()
Na novém místě plátna požádáme želvu o vykreslení čtverce
druhou metodou (cyklem). Jestli jsme se nespletli, budou oba
obrázky identické.
square_loop()
Příkazem (procedurou) done želvě oznámíme, že máme vše
vykresleno a program má vyčkat na ukončení uživatelem.
V této ukázce sestrojíme „segmentovaný“ šestiúhelník složením
z 6 pootočených rovnostranných trojúhelníků. Smyslem je ukázat,
že část výpočtu si můžeme pojmenovat, a poté ji s výhodou využít
jako stavební kámen něčeho složitějšího. V tomto případě se vybízí
pojmenovat si právě vykreslení onoho rovnostranného trojúhelníku:
def triangle():
for i in range(3):
forward(100)
left(120)
To, co jsme právě udělali, se obecně jmenuje definice podprogramu.
V tomto případě se jedná konkrétně o proceduru, totiž
podprogram, kterého smyslem je provést nějaké akce (vedlejší
efekty). V našem případě je tedy triangle procedurou pro
vykreslení rovnostranného trojúhelníku. Naše nově definovaná
procedura triangle je k nerozeznání od těch zabudovaných
(knihovních), které známe z předchozí ukázky: left,
forward a pod.
def hexagon():
for i in range(6):
triangle()
left(360.0 / 6)
Teď již víme, že main je také procedura, tedy podprogram,
kterého smyslem je vykonat posloupnost akcí (typicky dalších
procedur).
Procedura, kterou jsme definovali v předchozí ukázce, totiž
taková, která provede fixní (pokaždé stejnou) posloupnost akcí,
není příliš zajímavá. Naštěstí lze procedury parametrizovat.
Podobně jako u knihovních procedur forward nebo left si
můžeme sami definovat proceduru, které pak při použití
předáme nějaké číslo (obecněji hodnotu). Konkrétní předaná
hodnota pak bude mít vliv na chování takto definované procedury.
Zde si definujeme proceduru square, která se nápadně podobá na
proceduru square_loop z první ukázky, s jedním rozdílem: délka
strany již není pevně daná, ale je nyní proceduře předána jako
parametr.
def square(size):
for i in range(4):
left(90)
forward(size)
Takto definovanou proceduru můžeme opět používat zcela analogicky
k těm zabudovaným – nyní včetně předání parametru, který diktuje,
jak velký čtverec si přejeme vykreslit.
def main(): # demo
speed(5)
square(100)
Připomínáme, že následující tři příkazy slouží pouze k přesunu
želvy na jinou pozici na plátně.
Doposud jsme se nezabývali otázkou, odkud pochází definice
procedur left, forward apod. Protože ale v této ukázce budeme
potřebovat další knihovní podprogramy, je čas zmínit existenci
příkazu import. Tím oznámíme interpretu Pythonu, že hodláme
využívat podprogramy z externích modulů. V tomto kurzu se
omezíme na moduly ze standardní knihovny, totiž takové, které
jsou dodávány s každým interpretem jazyka Python.
Pro úplnost dodáme, že modul je sbírka vzájemně souvisejících,
znovupoužitelných podprogramů (a případně i složitějších
artefaktů, kterými se ale nebudeme v tomto kurzu příliš zabývat).
Krom procedur pro práci se želvou budeme v tomto příkladu
potřebovat několik matematických funkcí:
odmocninu, realizovanou podprogramem sqrt,
převod stupňů na radiány, realizovaný podprogramem radians,
goniometrickou funkci tangens, realizovanou podprogramem
tan.
Podprogramům, které realizují výpočet nějaké hodnoty na základě
hodnot svých parametrů, budeme říkat čisté funkce, z důvodu
jejich podobnosti s funkcemi z matematiky. Podprogramy sqrt,
radians a tan jsou tedy v tomto smyslu (čistými) funkcemi.
from math import sqrt, radians, tan
Krom použití funkcí si v této ukázce předvedeme také použití
proměnných. V nejjednodušším smyslu je proměnná pouze
pojmenováním nějaké vypočtené hodnoty – takto je budeme nyní
používat. Složitější případy použití proměnných (zejména
přiřazení) si necháme na příští týden.
Obrázek, který budeme kreslit, je rovnoramenný trojúhelník, zadaný
délkou základny a úhlem (v stupních) mezi základnou a ramenem.
def isosceles(base, angle):
První hodnotou, kterou si pojmenujeme (uložíme do proměnné)
bude polovina základny: rovnoramenný trojúhelník si totiž
pomyslně rozdělíme na dva stejné (pouze zrcadlově otočené)
pravoúhlé trojúhelníky s odvěsnami height (výška) a
half_base (polovina základny).
half_base = float(base) / 2
Protože trojúhelník máme zadaný základnou a přilehlým úhlem,
potřebujeme vypočítat délku ramene. To se nejsnadněji provede
pomocí už zmíněného pomyslného pravoúhlého trojúhelníku. Na
výpočet délky ramene použijeme Pythagorovu větu, ale nejprve
potřebujeme znát výšku (druhou z odvěsen pomyslného
trojúhelníku). Protože máme úhel zadaný v stupních, musíme ho
nejprve převést na radiány, pak jednoduše použijeme funkci
tangens, která udává poměr odvěsen v pravoúhlém trojúhelníku
(protilehlá k přilehlé). Výšku získáme jednoduchou úpravou
definičního výrazu.
height = half_base * tan(radians(angle))
Konečně můžeme přistoupit k výpočtu délky ramene:
side = sqrt(height ** 2 + half_base ** 2)
Nyní máme vše, co k vykreslení potřebujeme. Nejprve nakreslíme
základnu, poté želvu otočíme o vedlejší úhel k angle (tak,
aby úhel sevřený základnou a ramenem, které budeme kreslit
jako další byl angle). Vrcholový úhel je daný vztahem 180 -
2 * angle, nicméně opět potřebujeme želvu otočit o příslušný
vedlejší úhel (hodnotu 2 * angle dostaneme opět jednoduchou
úpravou). Nakonec vykreslíme druhé rameno, a želva se tím
vrátí do výchozí pozice.
Tato (pro tento týden poslední) ukázka předvede použití příkazu
if, který slouží k podmíněnému vykonání nějaké akce. Nejprve si
ale definujeme pomocnou proceduru triangle, která by nás již
neměla překvapit: vykresluje tupoúhlý, rovnoramenný trojúhelník,
který bude sloužit jako lupínek květiny. Důležitou vlastností této
procedury je, že zachová pozici i orientaci želvy.
Vykreslíme nyní stylizovanou květinu, které ale chybí některé
lupínky: konkrétně ty, jejichž pořadové číslo je dělitelné třemi
nebo pěti. Květinu budeme vykreslovat v cyklu, jak už je zvykem.
To, čím se tato ukázka liší od předchozích, je, že samotná
posloupnost akcí, které se v těle cyklu provedou, se bude iteraci
od iterace lišit. Parametr nám zadává původní počet lupínků (kolik
by jich bylo, kdyby žádný nechyběl).
def flower(petals):
for i in range(petals):
Podmínku zapisujeme klíčovým slovem if, následovaným
výrazem, který se vyhodnotí na booleovskou hodnotu (tzn.
True nebo False) a za dvojtečkou seznamem příkazů,
které se provedou pouze, vyhodnotil-li se předaný výraz
na hodnotu True (tzn. byl pravdivý).
V tomto případě se dotazujeme, zda má indexová proměnná
i nenulový zbytek po dělení jak číslem 3 tak číslem 5:
znamená to, že ani jeden z nich není dělitelem. Všimněte
si, že podmínku pro „chybějící“ lupínek jsme negovali:
lupínek vykreslíme, je-li tato (negovaná) podmínka
splněna, tedy bude chybět v případě, že byla splněna
původní podmínka ze zadání.
Budete-li srovnávat zápis programu s obrázkem,
který kreslí, je důležité si uvědomit, že první index je 0
(a je tedy dělitelný například i 3), nultý lupínek bude
tedy chybět. Kdyby nechyběl, „ukazoval“ by směrem doprava.
if i % 3 != 0 and i % 5 != 0:
triangle()
Bez ohledu na to, zda jsme lupínek vykreslili nebo
nikoliv, musíme se pootočit k vykreslení (nebo přeskočení)
dalšího lupínku: tento příkaz se provede v každé iteraci.
Protože se pootočíme doprava, lupínky vykreslujeme ve
směru hodinových ručiček (přičemž nultý by ukazoval
3 hodiny) – ve stejném směru, kterým ukazují vrcholy
trojúhelníků, které lupínky reprezentují.
Implementujte proceduru right_triangle, která vykreslí pravoúhlý
trojúhelník s odvěsnami o délkách side_a a side_b. Můžou se
vám hodit funkce z modulu math.
Zobecněte řešení z příkladu pentagon tak, abyste byli schopni
vykreslit libovolný pravidelný mnohoúhelník. Toto obecné řešení
implementujte jako proceduru polygon s parametry:
Nakreslete rovnoramenný lichoběžník s délkami základen
base_length a top_length a výškou height (lichoběžník je
čtyřúhelník s jednou dvojicí rovnoběžných stran – základen –
spojených rameny, které jsou obecně různoběžné).
Napište program, který nakreslí „plot“ o délce length pixelů,
složený z prken (obdélníků) o šířce plank_width a výšce
plank_height. Přesahuje-li poslední prkno požadovanou délku
plotu, ořežte jej tak, aby měl plot přesně délku length.
Zamyslete se nad rozdělením vykreslování do několika samostatných
procedur. Při kreslení se vám také může hodit while cyklus.
Implementujte proceduru spiral, která vykreslí čtyřhrannou
spirálu s rounds otočeními (počet otočení říká, kolik hran
musíme překročit, vydáme-li se ze středu spirály po přímce
libovolným směrem). Parametr step pak udává počet pixelů,
o který se hrany postupně prodlužují.
Implementujte proceduru heartbeat, která vykreslí stylizovanou
křivku EKG. Parametr iterations udává počet tepů, které
procedura vykreslí. Zbylé parametry zadávají amplitudu základního
úderu a periodu slabšího úderu. Slabší úder má poloviční
amplitudu. Například při periodě 3 bude mít sníženou amplitudu
každý třetí úder, počínaje prvním.
Napište proceduru pro vykreslení stylizovaného diamantu. Tento se
skládá z mnohoúhelníků, které jsou vůči sobě natočené o vhodně
zvolený malý úhel (takový, aby byl výsledný obrazec pravidelný).
Každý mnohoúhelník má sides stran o délce length pixelů.
Pomocí procedury pro mnohoúhelníky si nejprve zkuste vykreslit
kružnici. Poté napište proceduru pro vykreslení kružnice o zadaném
poloměru radius. (Nápověda: srovnejte obvod kružnice a
pravidelného n-úhelníku). Kružnici nakreslete tak, aby její střed
ležel v bodě, ve kterém byla želva před použitím procedury
circle. Pro vypnutí a zapnutí kreslení použijte procedury
penup a pendown. Po dokreslení kružnice vraťte želvu zpět do
jejího středu.
Napište proceduru, která bude kreslit soustředné kružnice, a to
tak, že první má poloměr radius a zbytek je rovnoměrně rozložen
tak, aby bylo kružnic celkem count.
Nakreslete obrys šipky zadaných rozměrů (celková šířka width a
celková výška height) a s úhlem špičky angle. Šipka by měla
ukazovat v původním směru želvy. Želva nechť je po konci procedury
ve stejné pozici a orientaci jako před jejím začátkem.
‡ Pozor! Tento a následující příklad jsou založeny na rekurzi,
kterou budeme probírat až na konci kurzu. Nemusíte si tedy lámat
hlavu, pokud je neumíte vyřešit.
Nakreslete Kochovu vločku, která má stranu o délce size.
Parametr depth udává kolikrát se má provést dělení strany
vločky. Konstrukce začíná rovnostranným trojúhelníkem, přičemž
vločka vzniká opakovanou aplikací následovného postupu na všechny
úsečky, které v daném okamžiku tvoří obrazec:
vybranou stranu rozdělte na třetiny a prostřední část
odstraňte,
nad prostřední částí sestrojte rovnostranný trojúhelník bez
základny: danou stranu jste tak nahradili sekvencí 4 úseček:
2 zbývající krajní třetiny původní strany a 2 ramena přidaného
trojúhelníku,
Daná iterace končí rozdělením poslední úsečky, která vznikla
v iteraci předchozí. Proveďte celkem depth iterací. Testy
vykreslují vločku hloubky dělení (počet iterací) 0 až 3.
‡ Nakreslete Hilbertovu křivku se stranou délky size a počtem
dělení iterations. Hilbertova křivka vzniká, podobně jako
Kochova vločka, opakovaným dělením stávajícího obrazce na zmenšené
kopie sebe sama. Podrobnější návod, jak křivku nakreslit (na
papír), naleznete na adrese https://is.muni.cz/go/9fh9k4.
Nakreslete domeček „jedním tahem“ (viz obrázky níže). Obdélníková
část domečku má šířku width a výšku height (kladná reálná
čísla), úhel špičky střechy je roof_angle stupňů (v rozsahu 1 až
179).
Nakreslete hvězdu (viz obrázky níže) s points paprsky. (Počet
paprsků je kladné celé číslo větší než 2). Paprsky hvězdy jsou
tvořeny rovnoramennými trojúhelníky bez základny, jejichž výška je
size (kladné číslo) a úhel svíraný rameny je angle (v rozsahu
1 až 179). Paprsky jsou rovnoměrně rozmístěny do kruhu. Jeden
z paprsků vždy směřuje na sever.
Poznámka: S extrémními hodnotami parametrů může výsledná „hvězda“
spíše připomínat zakulacený mnohoúhelník nebo ozubené kolo.
Nakreslete obrys vlajky s klínem vlevo (viz obrázky níže).
Parametry width a height (kladná reálná čísla) označují šířku,
resp. výšku vlajky. Parametr triangle_ratio (reálné číslo mezi 0
a 1 včetně) označuje, do jaké části šířky vlajky má zasahovat její
klín.
První kapitola sbírky slouží k procvičení látky z první přednášky –
tento princip bude v platnosti celý semestr.
Připomínáme, že příklady ze sekce příprav jsou bodované a v každém
čtyřtýdenním bloku musíte získat celkem alespoň 50 bodů (jakou
část získáte za přípravy je už nicméně na Vás). Abyste získali za
přípravy body, musíte je odevzdat vždy do soboty 23:59. Detailněji
jsou pravidla popsána v části A.
Tento týden se budeme zabývat zejména tzv. tokem řízení (anglicky
control flow) – téma, které jsme načali už v nultém týdnu. Jedná
se zejména o konstrukci podmíněného vykonání kódu (příkaz if) a
o konstrukce pro opakované spuštění sekvence příkazů (příkazy for,
while). V menší míře se budeme zabývat také proměnnými –
pojmenovanými hodnotami, vhodnými pro pozdější (případně
vícenásobné) použití.
V ukázkách si na příkladech vysvětlíme již zmiňované základní
konstrukce (teorii již znáte z přednášky). Ukázky označené znakem †
jsou náročnější – pravděpodobně se u nich budete muset více
soustředit. Nepovede-li se Vám takovou ukázku rozluštit napoprvé,
zkuste ji na pár dnů odložit, a vrátit se k ní později (poté, co se
Vám látka pro daný týden více rozležela v hlavě a již jste si
vyřešili pár příkladů).
triangle – návratové hodnoty podprogramů, funkce
sum – použití indexů v cyklech
fibonacci – přepis matematické posloupnosti do algoritmu
cycle – použití podmíněného příkazu
converge † – výběr podposloupnosti
Dále máte k dispozici několik elementárních příkladů, na kterých si
můžete nové konstrukce rychle procvičit:
divisors – zjištění počtu dělitelů čísla použitím cyklu
powers – součet po sobě jdoucích -tých mocnin
multiples – počítání násobků
Dalším krokem jsou samozřejmě již zmiňované přípravy. Ty, které
hodláte odevzdat, vypracujte zcela samostatně, u těch zbývajících
můžete pracovat způsobem, který Vám nejvíce vyhovuje: samostatně,
probrat myšlenku se spolužáky, ale naprogramovat každý sám, dokonce
si můžete vzájemně pomáhat i se samotným zápisem kódu. Ujistěte se
ale, že v žádném případě neodevzdáváte příklad, se kterým Vám
někdo pomáhal, a nepomáhejte spolužákům s příklady, které sami
hodláte odevzdat!
sequence – -té číslo posloupnosti s parametry
nested – vnořené posloupnosti
triples – největší pythagorejská trojice
geometry – predikáty trojúhelníkových vlastností
fibsum – suma sudých členů Fibonacciho posloupnosti
next – výpočet následujícího většího násobku
V předposlední sekci jsou rozšířené příklady: některé z nich si
vyřešíte příští týden na cvičení, ostatní můžete řešit se spolužáky
nebo samostatně jako přípravu na zkoušku. K těmto příkladům
naleznete v kapitole K vzorová řešení: silně Vám ale doporučujeme na
řešení se nedívat, dokud příklad nemáte vyřešený, nebo jste se u něj
vysloveně nezasekli.
even – součet sudých mocnin
prime – kontrola prvočíselnosti
coins – minimální počet mincí pro hodnotu
fibfibsum † – použití posloupnosti k indexaci
abundant † – vlastnosti čísel a jejich dělitelů
amicable † – vlastnosti dvojic čísel
Poslední částí jsou tzv. volitelné příklady. Ty si můžete vypracovat
dle libovůle samostatně nebo ve skupině, na rozdíl od příkladů typu
r však k těmto příkladům řešení nepřikládáme.
lvseq – -tý prvek jednoduché parametrické posloupnosti
Jak jsme již v předchozí kapitole zmínili, v tomto kurzu budeme
programovat v omezené podmnožině jazyka Python. Každá kapitola
v úvodní části představí všechny jazykové prostředky, které dosud
neznáte.
Výrazy v Pythonu intuitivně odpovídají výrazům, které znáte
z matematiky: skládají se z konstant, proměnných, operátorů,
závorek a volání funkcí (o funkcích detailněji níže). Každý výraz
má hodnotu, a smyslem výrazů je kompaktně popsat výpočet této
hodnoty. Příklady:
a // b, a % b – celočíselné dělení a zbytek po dělení
(připouštíme pouze pro dva celočíselné operandy),
a / b – dělení s desetinným výsledkem (naopak připouštíme
pouze v případě, kdy alespoň jedno z a, b je číslo
s plovoucí desetinnou čárkou – float),
a ** b – mocnění ,
relační (význam opět známe z matematiky):
a == b – rovnost,
a != b – různost / nerovnost,
a > b, a < b – ostré nerovnosti,
a >= b, a <= b – neostré nerovnosti,
logické (odpovídají logickým spojkám):
a and b – logická konjunkce: platí a a b zároveň
(vyhodnotí-li se a na False, podvýraz bnebude
vůbec vyhodnocen protože již nemůže výsledek ovlivnit),
a or b – logická disjunkce: platí alespoň jedno z a, b
(podobně, vyhodnotí-li se a na True, podvýraz b se
nevyhodnocuje).
Navíc jsou k dispozici dva unární operátory (mají pouze jeden
operand):
-a – opačná hodnota,
not a – logická negace.
Výrazem je také tzv. ternární operátor, který má podobu x if cond
else y – vyhodnotí-li se podvýraz cond na pravdivou hodnotu,
celý výraz se vyhodnotí na výsledek podvýrazu x, v opačném případě
na výsledek y (nepoužitý podvýraz se nevyhodnocuje).
Několik dalších operátorů (resp. nových významů stejných operátorů)
ještě přibude v příštích týdnech.
Dalším stavebním prvkem programu je příkaz, který odpovídá pokynu
k provedení nějaké akce. Nejjednodušší příkaz je tvořen libovolným
výrazem (užitečnost takových příkazů úzce souvisí s podprogramy,
které nejsou čistými funkcemi, obzvláště pak s procedurami).
Efektem takového příkazu je, že program vypočte jeho hodnotu a pak
ji zapomene.
Druhým základním typem příkazu je přiřazení, které podobně jako
v předchozím případě vypočte hodnotu výrazu, ale na rozdíl od
předchozího si ji zároveň zapamatuje a pojmenuje. Takto
pojmenovanou hodnotu – proměnnou – pak můžeme s výhodou použít
v pozdějších výrazech.7 V obou případech platí, že 1 řádek = 1
příkaz.
Přiřazení zapisujeme jako jméno = výraz, například:
a = 2
b = a + 1
b = -b
average = (a + b) / 2
positive = a > 0
Krom obyčejného přiřazení můžeme použít ještě tzv. složené
přiřazení, které umožňuje zápis některých častých operací zkrátit.
Tato složená přiřazení zapisujeme (věnujte pozornost závorkám a
rozdílu mezi / a //):
složené přiřazení
ekvivalentní zápis
a += 2
a = a + 2
x -= 2 * b
x = x - (2 * b)
a *= b + 2
a = a * (b + 2)
x /= a + b
x = x / (a + b)
x //= 3
x = x // 3
Pozor! Znak = v přiřazení není operátor a přiřazení není
výraz – např. zápis (a = b) + 3 nepřipouštíme.
Posledním typem příkazu, který zde uvedeme, je tzv. tvrzení, které
vyhodnotí zadaný výraz a je-li tento pravdivý, neudělá nic.
V opačném případě ukončí program s chybou. Příklad:
assert x > 0
Tento příkaz budete prozatím potkávat zejména v přiložených testech.
Samotné přiřazení nijak s hodnotami nemanipuluje, zejména je nevytváří ani nekopíruje. Význam přiřazení je skutečně pouze pojmenování hodnoty, která už musí existovat (obvykle jako výsledek vyhodnocení výrazu). Prozatím tento rozdíl není příliš důležitý – na chování programů začne mít dopad až ve třetí kapitole, kdy do jazyka přidáme složené typy. Pozor: některé programovací jazyky dávají přiřazení úplně jiný význam!
Krom výpočtu a zapamatování si hodnot potřebujeme pro zápis
algoritmů ještě rozhodování a opakování. K tomu slouží příkazy
toku řízení, konkrétně if, for a while.
Příkaz if realizuje rozhodnutí na základě pravdivostní hodnoty
(výrazu). Nejjednodušší forma je:
if podmínka₁:
příkaz₁
…
příkazₙ
Význam tohoto zápisu je: vypočti hodnotu výrazupodmínka₁ a
je-li výsledek pravdivý, proveď příkazypříkaz₁ až příkazₙ,
jinak nedělej nic (výpočet pak pokračuje dalším příkazem
v sekvenci). Příkaz if lze rozšířit o tzv. else větev:
if podmínka₁:
příkazy₁
else:
příkazy₂
který se chová stejně, ale v případě, že podmínka splněna nebyla,
ještě vykoná příkazy z posloupnosti příkazy₂. Konečně nejobecnější
podoba podmíněného příkazu je (vpravo ekvivalentní zápis pomocí
výše uvedené formy):
if podmínka₁: ⋅ if podmínka₁:
příkazy₁ ⋅ příkazy₁
elif podmínka₂: ⋅ else:
příkazy₂ ⋅ if podmínka₂:
⋅ příkazy₂
elif podmínka₃: ⋅ else:
příkazy₃ ⋅ if podmínka₃:
⋅ příkazy₃
else: ⋅ else:
příkazy₄ ⋅ příkazy₄
přičemž větví elif může být libovolný počet.
Pro opakování nějaké posloupnosti příkazů slouží cykly, které
jsou dvojího typu: for a while. Cyklus for použijeme
v případě, kdy předem známe počet iterací (opakování), které
chceme provést:
for jméno in rozsah:
příkazy
kde rozsah může být:
range(počet) – vypočte hodnotu výrazupočet a provede
sekvenci příkazy právě počet-krát (jméno je v -té
iteraci vázáno na hodnotu ),
range(od, do) – vypočte hodnoty výrazůod, do a
provede sekvenci příkazy pro hodnoty (jméno je
přitom opět vázáno na hodnotu ),
range(od, do, krok) – podobně jako předchozí, ale provede
sekvenci pro hodnoty kde:
je výsledek vyhodnocení výrazu krok,
je pro nebo jinak
a jméno je vázáno na hodnoty v pořadí stoupajícího .
Naopak cyklus while použijeme v situaci, kdy umíme výrazem popsat,
chceme-li provést další iteraci:
while podmínka:
příkazy
nejprve vyhodnotí výrazpodmínka. Je-li hodnota pravdivá,
provede příkazy a výraz podmínkaopět vyhodnotí. Cyklus je
ukončen v okamžiku, kdy se podmínka vyhodnotí jako nepravdivá
(v takovém případě už se příkazy neprovedou, může tedy nastat
situace, kdy se příkazy neprovedou ani jednou).
Kdekoliv v těle cyklu (ale nikde jinde) se mohou objevit ještě
příkazy break a continue (vztahují se k rozsahem nejmenšímu
cyklu, v kterého těle jsou obsaženy – tzn. k „nejvnitřnějšímu“
aktivnímu cyklu) a mají následovný význam:
continue okamžitě ukončí probíhající iteraci: program pokračuje
další iterací (není-li to možné, cyklus je na tomto místě
ukončen),
Podprogramy jsou základním stavebním prvkem složitějších programů.
Podprogram (v Pythonu také zvaný funkce) zastřešuje ucelený úsek
kódu, který má navíc název, parametry a návratovou hodnotu.
Podprogram definujeme následujícím zápisem:
kde podprogram je jméno, parametr₁ až parametrₙ jsou jména
tzv. formálních parametrů a příkaz₁ až příkazₙ jsou sekvencí
příkazů, které tvoří tzv. tělo podprogramu.
V podprogramu se krom už známých příkazů může objevit příkaz return
výsledek, který jeho vykonávání ukončí a určí návratovou
hodnotu (výsledek), kterou získá vyhodnocením výrazuvýsledek.
Chceme-li již definovaný podprogram (funkci) použít, slouží k tomu
tzv. volání funkce. Volání je výraz, a zapisuje se následovně:
podprogram(výraz₁, výraz₂, …, výrazₙ)
Zde podprogram je jméno a výraz₁ až výrazₙ jsou tzv.
skutečné parametry. Protože se jedná o výraz, má hodnotu, která
odpovídá návratové hodnotě podprogramu (příkazu return, kterým byl
ukončen). S touto hodnotou můžeme pracovat jako s libovolným jiným
výrazem:
Krom podprogramů, které si sami definujete, můžete využívat několik
takových, které jsou v jazyce zabudované (jsou součástí jazyka).
Seznam těchto podprogramů budeme během semestru postupně rozšiřovat.
Prozatím jsou to tyto (všechny zde uvedené podprogramy jsou zároveň
čisté funkce):
min(a, b) a max(a, b): vybere nejmenší, resp. největší
hodnotu mezi svými parametry,
abs(x): spočte absolutní hodnotu parametru x,
round(x): pro desetinné číslo x se vyhodnotí na nejbližší
celé číslo (hodnoty přesně mezi se zaokrouhlí na nejbližší sudé
číslo),
float(x): pro celé číslo x se vyhodnotí na odpovídající číslo
s plovoucí desetinnou čárkou (v případě, že konverzi provést
nelze, protože x příliš velké, je program ukončen s chybou).
Dále máte k dispozici proceduruprint, kterou si můžete pomoct
při programování, ale kterou jinak v tomto kurzu budeme potřebovat
jen výjimečně.
můžeme požádat o zpřístupnění podprogramů nebo konstant name₁,
name₂ atd. z knihovnymodule. V této chvíli můžete používat
pouze tyto čisté funkce, které realizují výpočet funkcí
v matematickém smyslu, a konstanty z knihovny math:
pi – číslo (poměr obvodu a průměru kružnice),
goniometrické a cyklometrické funkce:
cos(x), sin(x), tan(x) – známé goniometrické funkce
(parametr x je zadán v radiánech),
acos(x), asin(x) – cyklometrické (inverzní trigonometrické)
funkce, vstupem je reálné číslo intervalu a výsledkem
je odpovídající úhel z intervalu ,
atan(x) – inverzní funkce k funkci tan (vstupem je
libovolné reálné číslo, výsledkem úhel z intervalu ,
atan2(y, x) – úhel svíraný x-ovou osou a polopřímkou
z počátku, která prochází bodem , v rozsahu ,
funkce pro převod úhlů:
radians(x) – stupně na radiány a
degrees(x) – radiány na stupně,
funkce pro výpočet kořenů:
sqrt(x) – druhá odmocnina reálného čísla x a
isqrt(x) – největší celé číslo menší rovno odmocnině x,
funkce pro převod reálných čísel na celá (viz též zabudovanou
funkci round uvedenou výše):
trunc(x) – ořezání desetinné části,
floor(x) – největší celé číslo ≤ x,
ceil(x) – nejmenší celé číslo ≥ x,
funkce isclose(x, y) která realizuje „přibližnou rovnost“ čísel
s plovoucí desetinnou čárkou.
Abychom demonstrovali zápis a použití (čistých) funkcí a tedy i
návratových hodnot, zadefinujeme si jednoduchou funkci se třemi
parametry: délkami stran, které můžou (ale nemusí) zadávat
trojúhelník. Výsledkem je pravdivostní hodnota (True nebo
False), která říká, zda zadaná trojice délek stran skutečně
popisuje přípustný trojúhelník. Funkcím, které nemají vedlejší
efekty (tj. čistým), a kterých výsledkem je pravdivostní hodnota,
říkáme predikáty.
Funkce, stejně jako procedury, definujeme klíčovým slovem def,
za kterým následuje název funkce. Názvy (a později v semestru i
typové anotace) parametrů píšeme do závorek za název funkce a
oddělujeme je čárkami. V tomto kontextu mluvíme o formálních
parametrech – v těle funkce se chovají jako proměnné, do kterých
jsou přiřazeny hodnoty tzv. skutečných parametrů – těch, které
jsou funkci předány při jejím použití (viz také níže). Řádek
ukončíme dvojtečkou a pokračujeme tělem funkce: seznamem
příkazů, které se při jejím použití (zavolání) vykonají.
def is_triangle(a, b, c):
Vykonávání funkce je (korektně) ukončeno buď dojdou-li příkazy
k vykonání (dojdeme „na konec“), nebo vykonáním příkazu return.
Chceme-li, aby funkce poskytla svému volajícímu nějaký výsledek,
musíme použít příkaz return, kterému tuto výslednou hodnotu
předáme. Výsledek můžeme zapsat jako libovolný výraz (zejména
tedy nemusí být uložen v proměnné).
Všimněte si, že v tomto případě je výsledkem funkce logická
konjunkce (použití operátoru and) tří podvýrazů, kde každý
popisuje jednu variantu tzv. trojúhelníkové nerovnosti. Za zmínku
zde stojí i konkrétní zápis těchto variant – první konjunkt je
zapsán v abecedním pořadí a každý další vznikl tzv. cyklickou
záměnou předchozího, tzn. náhradami a → b, b → c a c →
a.
return (a + b > c) and (b + c > a) and (c + a > b)
Procedura main je součástí každého příkladu, a obsahuje
jednoduché (základní) testy, které ověří, že jste naprogramovali
zhruba to, co se očekávalo. Procházející testy nezaručují, že
je Vaše řešení správné! U příkladů jsou testy pouze v kostrách
(nachystaných zdrojových souborech .py): v HTML a PDF verzi
sbírky je budeme zobrazovat jen v ukázkách jako je tato.
def main(): # demo
V tomto příkladu stojí za povšimnutí i samotný zápis testů (je
důležité, abyste je uměli přečíst): příkaz assert ověří, že
výraz, který mu předáváme, se vyhodnotí na hodnotu True,
a pokud tomu tak není, program okamžitě ukončí s chybou.
Krom použití příkazu assert si všimněte i zápisu tzv.
volání funkce (neboli jejího použití): volání funkce je
výraz, který začíná jménem příslušné funkce, které je
následováno závorkami, do kterých uvádíme (skutečné) hodnoty
parametrů funkce. Závorky mohou být prázdné, ale nelze je
vynechat.
assert is_triangle(3, 4, 5)
assert is_triangle(1, 1, 1)
assert not is_triangle(1, 1, 3)
assert not is_triangle(2, 3, 1)
Ujistěte se, že těmto definicím rozumíte: neznáte-li například
definici operátoru ∑ (suma), můžete se s výhodou obrátit na
Wikipedii. Pro jistotu uvádíme několik členů obou těchto
posloupností:
Naším úkolem bude nyní naprogramovat v Pythonu (čistou) funkci
nth_element(n), která počítá příslušné , a (opět čistou)
funkci partial_sum(n), která počítá příslušné . První funkce
je přímočará, stačí nám znát zabudovaný operátor mocnění ** a
zápis definice funkce:
def nth_element(n):
return n ** n
Výpočet partial_sum(n) bude nicméně o něco složitější: operátor
suma sčítá řadu čísel, jejichž počet je dán rozdílem mezi jeho
horním a dolním indexem. Objeví-li se v některém indexu proměnná,
počet sečtených členů bude typicky záviset na hodnotě této
proměnné.
Jak již jistě víte z přednášky, v situaci, kdy potřebujeme
opakovaně provádět příkazy (a zejména není-li počet opakování
konstanta) použijeme cyklus. Nejjednodušší formou cyklu je
příkaz „opakuj n-krát“, který v Pythonu zapisujeme for i in
range(n).
Krom hodnoty n je zde důležitá ještě proměnná i: obecně se
jedná o tzv. proměnnou cyklu. Tato proměnná má k tělu cyklu
podobný vztah, jako má parametr funkce k tělu funkce: před každým
provedením těla (tzv. iterací) se do i přiřadí nová hodnota
(jaká přesně hodnota to bude záleží na konkrétní formě cyklu).
V tomto případě – cyklus tvaru for i in range(n) – se do i
přiřadí pořadové číslo iterace, a samotnou proměnnou i pak
nazýváme indexovou proměnnou. Ve většině programovacích jazyků
(a Python není výjimkou) se indexuje od 0, tzn. v první iteraci
je i = 0, ve druhé i = 1, atd., konečně v poslední iteraci je
i = n - 1. Nyní můžeme konečně přistoupit k definici funkce
partial_sum(n):
def partial_sum(n):
Jako první krok si zavedeme proměnnou, do které budeme
postupně přičítat jednotlivé hodnoty – takové proměnné
říkáme střadač nebo akumulátor (angl. accumulator).
result = 0
Následuje samotný cyklus, který v každé iteraci do akumulátoru
result přičte příslušnou hodnotu . Protože indexová
proměnná i je číslována od 0, ale hodnoty jsou
číslovány od 1, vypočteme hodnotu jako nth_element(i +
1):
for i in range(n):
result += nth_element(i + 1)
Po skončení cyklu je v akumulátoru požadovaná suma . Pro každé i v rozmezí 0 až n - 1 (včetně) bylo
provedeno tělo cyklu, a v result je tedy uložen součet
nth_element(0 + 1) + nth_element(1 + 1) + ... + nth_element(n
- 1 + 1), neboli nth_element(1) + nth_element(2) + ... +
nth_element(n).
(Čistá) funkce fib počítá n-tý prvek tzv. Fibonacciho
posloupnosti, dané předpisem: -- každý prvek této posloupnosti je tedy součtem
předchozích dvou (s výjimkou prvních dvou, které jsou pevně dané).
Zkusíte-li si posloupnost napsat na papír (1, 1, 2, 3, 5, …),
zřejmě zjistíte, že nejjednodušší způsob jak to udělat, je sečíst
vždy poslední dvě už napsaná čísla a výsledek připsat na konec
vznikajícího seznamu. Na dřívější čísla se už nemusíme znovu
dívat: pro výpočet dalšího prvku potřebujeme vidět právě dva
předchozí prvky. Můžete tedy vzít gumu, a po připsání jednoho
čísla na konec smazat jedno číslo ze začátku – ani s tímto
opatřením nebudete mít s výpočtem žádný problém. Na papíře budou v
každém momentě 2 nebo 3 čísla, podle toho, kde se ve výpočtu
nacházíte.
Tuto myšlenku využijeme pro zápis algoritmu: budeme
potřebovat dvě proměnné, které budou reprezentovat ony dvě
„naposled zapsaná“ čísla na konci posloupnosti (protože někdy máme
ale na papíře čísla 3, budeme ve skutečnosti občas potřebovat
ještě jednu – dočasnou – proměnnou).
Protože postup výpočtu sleduje fixní seznam kroků, který se dokola
opakuje, použijeme navíc cyklus.
def fib(n):
Proměnná a reprezentuje předposlední a proměnná b poslední
vypočtené Fibonacciho číslo. Na začátku jsme na papír napsali
dvě jedničky – jedná se o ony pevně dané první dva prvky
posloupnosti.
a = 1
b = 1
Zatím jsme „vypočítali“ první a druhé Fibonacciho číslo.
Zajímá-li nás n-té číslo, musíme připsat dalších n - 2
čísel, aby platilo, že poslední číslo je to, které nás zajímá.
V každé iteraci následujícího cyklu provedeme výpočet jednoho
dalšího čísla (a umazání prvního čísla).
for i in range(n - 2):
Do nové (dočasné) proměnné c si vypočteme další
Fibonacciho číslo. Po tomto příkazu bude proměnná a
obsahovat třetí číslo od konce aktuálně „zapsaného“
seznamu, proměnná b číslo předposlední a proměnná c
číslo poslední. Jsme nyní v situaci, kdy si pamatujeme
zároveň 3 čísla.
c = a + b
„Zapomenutí“ prvního čísla realizujeme tak, že „nové“
poslední dvě čísla (nyní b a c) uložíme do proměnných
a a b. Hodnotou uloženou v (dočasné) proměnné c se
nebudeme dále zabývat – v další iteraci cyklu proměnnou
c přepíšeme novou dočasnou hodnotou. Zamyslete se, zda
je pořadí následujících dvou příkazů důležité, a proč.
a = b
b = c
Jak jsme zmínili na začátku, proměnná b reprezentuje poslední
vypočtené Fibonacciho číslo (s výjimkou krátkého okamžiku
uprostřed cyklu). Protože jsme vypočetli právě n čísel,
poslední z vypočtených čísel je n-té, a tedy proměnná b
obsahuje kýžený výsledek funkce fib.
Uvažujme posloupnost definovanou jako , kde
se cyklicky vybírá z . Prvních 5 prvků této
posloupnosti (zařazené v OEIS jako A047908) je:
Naším úkolem bude napsat (čistou) funkci, která vyčíslí n-tý
prvek této posloupnosti:
def cycle(n):
Protože budeme chtít použít cyklus while, musíme si
indexovou proměnnou explicitně zavést:
i = 1
K výpočtu potřebujeme znát hodnotu , proto si
aktuální hodnotu uložíme do proměnné a_i (podobně
jako jsme k výpočtu Fibonacciho posloupnosti potřebovali
poslední dva prvky). V další iteraci (poté, co se zvýší
indexová proměnná i) budeme mít v a_i chvíli hodnotu
, kterou využijeme pro výpočet (nové) hodnoty .
a_i = 1
Cyklus while, jak jistě víte z přednášky, provádí své tělo
tak dlouho, dokud platí podmínka cyklu. V tomto případě tedy
budeme cyklus opakovat dokud platí i < n:
while i < n:
Nyní se musíme rozhodnout, který operátor použít pro
výpočet další hodnoty a_i. Protože cyklicky vybíráme ze
3 možností, můžeme se rozhodnout dle zbytku po dělení
indexu i třemi: v první, čtvrté, sedmé atd. iteraci
použijeme operátor +, v druhé, páté, ... operátor * a
konečně ve třetí, šesté, ... operátor -:
if i % 3 == 1:
a_i = a_i + i
elif i % 3 == 2:
a_i = a_i * i
else: # i % 3 == 0
a_i = a_i - i
i += 1
V každé iteraci cyklu zvyšujeme indexovou proměnnou i
o jedna, a před cyklem platilo i ≤ n. Po cyklu musí tedy
nutně platit i == n, a protože zároveň po každé iteraci
platí, že a_i obsahuje hodnotu , musí také platit, že po
ukončení cyklu je v proměnné a_i uložena hodnota .
† Každá omezená posloupnost – tedy taková, která nabývá hodnoty
pouze z nějakého konečného intervalu – má tzv. konvergentní
podposloupnost. Co tyto termíny přesně znamenají nás nemusí
trápit (více se dozvíte v matematické analýze): nám bude stačit
intuice.
Podposloupnost je posloupnost, která vznikne „přeskočením“
některých prvků původní posloupnosti (zde je podposloupnost
sestávající z lichých prvků posloupnosti ):
Konvergentní posloupnost je pak taková, že se její prvky postupně
blíží nějaké konkrétní hodnotě (tzv. limitě ) – přibližně
platí, že čím větší index , tím je vzdálenost
menší.
Naším úkolem bude nějakou takovou konvergentní podposloupnost
najít: začneme omezenou posloupností a budeme
budovat konvergentní podposloupnost B s prvky . Pozor: hledáme
libovolnou podposloupnost s potřebnou vlastností, nikoliv nějakou
konkrétní – máme tak při implementaci relativně velkou volnost.
Jak tedy na to?
První pozorování je, že se stačí zabývat kladnými hodnotami .
Dále pak stačí zabezpečit, aby platilo . Při výběru
hodnoty máme mnoho možností, ale je výhodné zvolit . Zapišme nyní funkci convergent(n), které výsledkem
bude hodnota :
def convergent(n):
Pro samotný výpočet budeme potřebovat dva indexy: index i
náleží posloupnosti (čísluje tedy prvky ) zatímco
index j náleží posloupnosti (čísluje prvky ).
i = 1
j = 1
Navíc si potřebujeme pamatovat poslední nalezenou hodnotu
– proměnná last bude vždy (opět s výjimkou krátkého okamžiku
mezi dvěma sousedními příkazy uvnitř cyklu) obsahovat j-tou
hodnotu posloupnosti (kde j značí hodnotu proměnné j
zavedené výše). Vzpomeňte si také, že .
last = sin(i)
Následuje samotný cyklus, který bude hledat hodnotu .
Tento bude postupně procházet prvky posloupnosti .
Vždy, když nalezneme nové , pro které platí –
kde je uloženo v proměnné last – můžeme toto
přidat do posloupnosti , jako , a odpovídajícím
způsobem upravit proměnné j a last. V programu zapisujeme
jako sin(i).
while j < n:
i += 1
if sin(i) > 0 and sin(i) <= last:
j += 1
last = sin(i)
Po ukončení cyklu platí j == n (před cyklem platilo j ≤ n,
cyklus ukončíme jakmile přestane platit j < n a zároveň
hodnotu j v každé iteraci zvýšíme nejvýše o 1). Protože
v každém kroku platí, že proměnná last obsahuje prvek a
nyní zároveň platí j = n, celkem dostáváme, že po ukončení
cyklu je v proměnné last uložena hodnota .
Krom obvyklých konkrétních případů, které testujeme výše,
můžeme ověřovat i vlastnosti námi implementovaných funkcí.
Například níže kontrolujeme monotónnost (posloupnost je
nestoupající) a omezenost zespodu (nulou). Tyto dvě vlastnosti
dohromady zaručují, že posloupnost je konvergentní:
samozřejmě, v konečném čase lze takto ověřit pouze konečný
počet případů, a testy nám tedy ani jednu ze zmiňovaných tří
vlastností nemohou zaručit.
for i in range(5):
assert convergent(i + 1) <= convergent(i)
assert convergent(i) > 0
Napište funkci, která vrátí počet různých kladných dělitelů
kladného celého čísla number (např. číslo 12 je dělitelné 1, 2,
3, 4, 6 a 12 – výsledek divisors(12) bude tedy 6.
Napište funkci sum_of_multiples s parametrem n, která spočítá
sumu kladných čísel , kde n a zároveň nebo
(t.j. každé je dělitelné třemi nebo pěti). Například
pro n = 10 je očekávaný výsledek .
Napište (čistou) funkci sequence, která spočítá hodnotu členu
níže popsané posloupnosti, kde n je první parametr této funkce.
První člen posloupnosti, , je zadán parametrem initial,
každý další člen je pak určen sumou ,
kde k je druhým parametrem funkce sequence. Například pro
parametry k = 3 a initial = 2 jsou první 3 členy posloupnosti:
Očekávaný výsledek pro volání sequence(2, 3, 2) je tedy 8.
Napište funkci nested, která spočítá n-tý člen posloupnosti
(počítáno od 0), která vznikne napojením postupně se
prodlužujících prefixů přirozených čísel.
Nechť je posloupnost čísel až :
Hledaná posloupnost vznikne napojením posloupností , ,
… (do nekonečna) za sebe:
Vaším úkolem je najít n-tý prvek posloupnosti .
def nested(n):
pass
Dále napište funkci nested_sum, která spočítá sumu prvních n členů
této posloupnosti.
Napište funkci largest_triple, která najde pythagorejskou
trojici – totiž takovou, že , a jsou
přirozená čísla a platí (tzn. tvoří pravoúhlý
trojúhelník). Hledáme trojici, která:
má největší možný součet ,
hodnoty , jsou menší než max_side.
Výsledkem funkce bude součet , tedy největší možný
obvod pravoúhlého trojúhelníku, jsou-li obě jeho odvěsny kratší
než max_side. Předpokládejte, že max_side bude vždy alespoň 5.
Napište predikát (tj. čistou funkci, která vrací pravdivostní
hodnotu – boolean), který je pravdivý, je-li možno vytvořit
pravoúhlý trojúhelník ze stran o délkách zadaných kladnými
celými čísly a, b a c.
def is_right(a, b, c):
pass
Dále napište predikát, který je pravdivý, popisují-li parametry
a, b a c rovnostranný trojúhelník.
def is_equilateral(a, b, c):
pass
Konečně napište predikát, který je pravdivý, popisují-li parametry
a, b a c rovnoramenný trojúhelník.
Napište funkci, která spočítá sumu prvních nsudých členů
Fibonacciho posloupnosti (tj. členů, které jsou sudé, nikoliv
těch, které mají sudé indexy). Například volání fibsum(3) = 44 =
2 + 8 + 34.
Uvažme, že chceme přesně zaplatit sumu value, přičemž máme
k dispozici pouze mince denominací 1, 2 a 5 korun.
Spočtěte, kolik nejméně mincí potřebujeme.
† Nechť je Fibonacciho posloupnost s členy a je
posloupnost taková, že má na -té pozici -tý prvek
posloupnosti , tj. prvek s indexem (nikoliv prvek
s indexem ). Napište funkci, která sečte prvních count prvků
posloupnosti (t.j. ty prvky posloupnosti , kterých indexy
jsou po sobě jdoucí Fibonacciho čísla).
† Napište predikát, který určí, jsou-li dvě kladná celá čísla
spřátelená (amicable). Spřátelená čísla jsou taková,
že součet všech vlastních dělitelů jednoho čísla se rovná
druhému číslu, a naopak – součet všech vlastních dělitelů
druhého čísla se rovná prvnímu.
Za vlastní dělitele čísla považujeme všechny jeho kladné
dělitele s výjimkou čísla samotného; např. vlastní dělitelé
čísla 12 jsou 1, 2, 3, 4, 6.
Napište čistou funkci sum_elements_dn, která vrátí součet
prvních count prvků vzestupně seřazené posloupnosti kladných
celých čísel, která jsou dělitelná číslem div a zároveň nejsou
dělitelná číslem nondiv. Předpokládejte, že všechny parametry
jsou kladná celá čísla a že číslo div není dělitelné číslem
nondiv. (Můžete zkusit přemýšlet, co by se stalo v takovém
případě.)
Napište čistou funkci largest_on_path která vrátí největší
číslo, na které narazíme, půjdeme-li dle níže popsaných kroků od
kladného celého čísla num po číslo 1. Povolené kroky jsou
následující:
je-li num sudé, vydělíme je dvěma,
je-li num liché a větší než 1, vynásobíme je třemi a_přičteme 1,
Tento týden pokračujeme v programování s čísly (první setkání se
složitějšími datovými typy nás čeká příští týden). Tentokrát si
naprogramujeme řadu jednoduchých algoritmů, které si vystačí
s konstrukcemi, které již známe: cykly for a while, podmíněnými
příkazy if, proměnnými, a definicemi čistých funkcí. Významnější
roli budou hrát i čísla s plovoucí desetinnou čárkou – typ float.
To, co bude tento týden nové je, že algoritmy, které budeme
programovat, budou mít složitější strukturu, budou používat více
proměnných a budou se typicky více větvit. V tomto týdnu byste si
tedy z cvičení měli odnést základní dovednosti algoritmizace a
v tomto kontextu si procvičit použití a zápis konstrukcí, které
znáte z prvních dvou přednášek.
V neposlední řadě dojde tento týden i na základy dekompozice:
některé algoritmy, které budeme programovat, bude vhodné rozložit na
podprogramy. Podobně jako minulý týden, budeme tento týden pracovat
pouze s čistými funkcemi: kdykoliv v příkladech pro tento týden
zmíníme funkci, myslíme tím implicitně funkci čistou.
Tato kapitola používá stejné jazykové prostředky a zabudované
podprogramy jako kapitola první. Přibyla pouze jediná knihovní
(čistá) funkce, a to factorial(n) z knihovny math, pro přímý
výpočet faktoriálu přirozeného čísla n.
K zápisu čísel v západní civilizaci běžně používáme desítkovou
soustavu. Desítková soustava je jednou z mnoha tzv. pozičních
číselných soustav, při kterých se hodnota čísla odvíjí od toho, na
jaké pozici stojí jaká číslice. Hodnotu čísla získáme tak, že pozice
číslujeme od nuly zprava, hodnotu každé číslice násobíme základem
umocněným na pozici a výsledky sečteme.
V desítkové soustavě tedy nejpravější číslici násobíme ,
druhou číslici zprava násobíme , třetí zprava
atd.
Můžeme ovšem za základ vzít i jiné číslo než je desítka. Třeba ve
trojkové soustavě násobíme číslice zprava hodnotami 1, 3, 9, 27, …
v sedmičkové soustavě násobíme číslice zprava hodnotami 1, 7, 49, 343.
To, že daný zápis je myšlen v soustavě s jiným základem než ,
typicky v matematice značíme uzávorkováním a dolním indexem.
Například je zápis čísla 162, protože .
Důležité je si uvědomit, že čísla (jako abstraktní pojem pro počet)
jsou úplně nezávislá na zvolené reprezentaci. Pokud bychom se
vyvinuli jinak a neměli deset prstů, ale třeba jen osm, tak by nám
desítková soustava připadala bizarní a osmičková jako zcela
přirozená. (A mimochodem, v historii se taky používala soustava
dvanáctková nebo šedesátková – zbytek té historie vidíme např. na
současném systému pro měření času.)
Hlavní myšlenkou zde je to, že , tedy jde o totéž číslo,
jen jinak zapsané.
V Pythonu máme standardně možnost používat tyto soustavy:
desítkovou (používáme číslice 0, 1, 2, 3, 4, 5, 6,
7, 8, 9 a zápis čísel nezačíná žádným speciálním prefixem),
dvojkou (používáme číslice 0, 1 a zápis čísel začíná 0b),
osmičkovou (používáme číslice 0, 1, 2, 3, 4, 5, 6,
7 a zápis čísel začíná prefixem 0o),
šestnáctkovou (používáme číslice 0, 1, 2, 3, 4, 5,
6, 7, 8, 9, a, b, c, d, e, f a zápis čísel
začíná prefixem 0x).
Tedy např. číslo 0o321 je číslo . Totéž číslo se
taky dá v Pythonu zapsat jako 209 nebo 0xd1 nebo 0b11010001,
ale pořád je to stejné číslo, jak dokládá i skutečnost, že výraz
0b11010001 == 209 se vyhodnotí na True.
V této ukázce si naprogramujeme jednoduchý algoritmus, který
pracuje s desítkovým rozvojem celého čísla: konkrétně se budeme
ptát, zda jsou v desítkovém zápisu daného čísla jednotlivé cifry
uspořádané sestupně (uvažujeme pořadí od nejvýznamnější, tzn.
nejlevější, cifry).
Protože chceme pracovat s ciframi, jeví se jako rozumné
zadefinovat si pomocnou funkci, která nám vrátí konkrétní cifru.
Desítkový rozvoj přirozeného čísla , které má desítkových
cifer, lze zapsat:
kde pro každé platí . Za povšimnutí stojí i
to, že dle zde použité definice má nejméně významná cifra
(„jednotky“) index 0.
Chceme-li nalézt -tou cifru, můžeme postupovat následovně:
nejprve vydělíme číslem – pohled na pravou stranu výše
uvedené rovnosti nám rychle napoví, že členy, u kterých je mocnina
desítky menší než ze sumy úplně zmizí a člen se stane
nejnižším (rozmyslete si, jak vypadá člen, kde ):
Zbývá učinit následovné pozorování: protože nás zajímá hodnota
, a protože každé jiné se v rozvoji objevuje
vynásobeno nějakou kladnou mocninou desítky, můžeme s výhodou
použít operaci zbytku po dělení (modulo, operátor %): tímto se
zbavíme všech ostatních členů (formálněji: zbytek po dělení členu
desíti je 0 pro každé ):
Tímto je vysvětlena na pohled velice jednoduchá funkce
get_digit:
Následující funkce pracuje na stejném principu: každé dělení
desíti odstraní jednu cifru (jeden člen sumy, která definuje
desítkový rozvoj). Počet provedených iterací si udržujeme v čítači
count.
def count_digits(number):
count = 0
while number > 0:
count += 1
number = number // 10
return count
Funkce get_digit a count_digits nám už umožní popsat náš
původní problém přirozeným způsobem: pro každou dvojici cifer
ověříme, že jsou ve správném pořadí. Protože cifry jsou při
procházení zleva očíslovány sestupně, musíme si dát pozor, v jakém
pořadí ony dvě srovnávané cifry následují.
def is_descending(number):
Dvojic cifer je o jednu méně, než cifer samotných: dvojciferné
číslo má jednu dvojici cifer, trojciferné dvě, atd., proto
musíme od výsledku count_digits odečíst jedničku.
for k in range(count_digits(number) - 1):
Označme cifry čísla number: volání funkce
get_digit(number, i) tedy vrací hodnotu . Cifra
s indexem k + 1 je nalevo od cifry s indexem k:
mají-li být tedy cifry uspořádány sestupně zleva doprava,
musí pro každou dvojici platit . Protože
kontrolujeme, že tato podmínka platí pro každou dvojici,
jakmile nalezneme nějakou, která ji porušuje (proto
v podmínce níže naleznete negaci „chtěné“ vlastnosti),
víme, že celkový výsledek je False, a vykonávání funkce
ukončíme příkazem return (na ostatní dvojice se už není
potřeba dívat).
if get_digit(number, k + 1) < get_digit(number, k):
return False
V cyklu výše jsme zkontrolovali každou dvojici cifer: kdyby
některá porušila kýženou vlastnost (cifry jsou uspořádané
sestupně), spustil by se příkaz return a funkce by byla
ukončena. Proto, dojdeme-li až sem, víme, že vlastnost platila
pro každou dvojici cifer, a tedy platí i pro číslo jako celek.
return True
Zbývá pouze ověřit, že jsme v implementaci neudělali chybu.
def main(): # demo
assert is_descending(7)
assert is_descending(321)
assert is_descending(33222111)
assert is_descending(9999)
assert is_descending(7741)
assert not is_descending(123)
assert not is_descending(332233)
assert not is_descending(774101)
V této ukázce se zaměříme na ekvivalenci for a while cyklů.
Podíváme se přitom na kombinační čísla, definovaná jako:
kde . Samozřejmě, mohli bychom počítat kombinační čísla
přímo z definice, navíc v modulu math je již k dispozici funkce
factorial, takže bychom se v zápisu obešli úplně bez cyklů.
Nicméně jednoduché pozorování nám (resp. programu, který bude
výpočet provádět) může ušetřit významné množství práce. Jak jistě
víte, faktoriál je definován takto:
A tedy:
Navíc, abychom měli zaručeno, že skutečně práci ušetříme, můžeme
tento trik aplikovat na větší z nebo .
def comb_for(n, k):
Nejprve zjistíme, které z resp. je menší: vzhledem
k symetrii definice vůči těmto dvěma hodnotám můžeme případně
nahradit hodnotou , aniž bychom změnili výsledek:
platí .
if k < n - k:
k = n - k
Dále chceme vynásobit všechna čísla mezi a (nicméně
samotné chceme přeskočit, zatímco chceme zahrnout):
numerator = 1
for i in range(k + 1, n + 1):
numerator *= i
return numerator // factorial(n - k)
Nyní ekvivalentní definice pomocí cyklu while:
def comb_while(n, k):
if k < n - k:
k = n - k
numerator = 1
i = k + 1
while i <= n:
numerator *= i
i += 1
return numerator // factorial(n - k)
Kontrolu správnosti tentokrát provedeme trochu jinak: nebudeme
kontrolovat předem vypočtené hodnoty, které bychom napsali do
programu jako konstanty, jak jsme to většinou dělali doteď. Místo
toho ověříme, že naše implementace dává stejný výsledek, jako
výpočet přímo z definice. Díky tomu můžeme kontrolovat výrazně
více případů, aniž bychom se takříkajíc upsali k smrti.
def main(): # demo
for n in range(1, 50):
for k in range(1, n):
naive = factorial(n) // (factorial(k) * factorial(n - k))
assert comb_for(n, k) == naive
assert comb_while(n, k) == naive
V této ukázce si napíšeme program, který bude počítat obvod
trojúhelníka, který ale může být zadaný různými způsoby: tři
strany, 2 strany a sevřený úhel, dva úhly a libovolná strana.
Strany budeme značit ; úhel mezi a bude
(gamma), mezi a bude (alpha) a konečně mezi a
je úhel (beta):
Nejjednodušší je samozřejmě výpočet obvodu pro trojúhelník zadaný
třemi stranami:
def perimeter_sss(a, b, c):
return a + b + c
Následuje trojúhelník zadaný dvěma stranami a sevřeným úhlem, kdy
získáme délku třetí strany použitím kosinové věty.
def perimeter_sas(a, gamma, b):
c = sqrt(a ** 2 + b ** 2 - 2 * a * b * cos(radians(gamma)))
return perimeter_sss(a, b, c)
Dále vyřešíme případ jedné strany a dvou jí přilehlých úhlů, kdy
použijeme naopak větu sinovou.
def perimeter_asa(alpha, c, beta):
gamma = radians(180 - alpha - beta)
alpha = radians(alpha)
beta = radians(beta)
a = c * sin(alpha) / sin(gamma)
b = c * sin(beta) / sin(gamma)
return perimeter_sss(a, b, c)
Poslední případ, který budeme řešit, jsou dva úhly a strana
přilehlá pouze druhému z nich. Tento případ lehce převedeme na
předchozí.
Tím končí samotná implementace, nyní přistoupíme k jejímu
testování. Asi si uvědomujete, že v předchozím byl relativně velký
prostor k překlepům a záměnám stran nebo úhlů. Proto budeme
testovat důkladněji, než bylo dosud obvyklé – budeme postupovat
podobně, jako v předchozí ukázce. Nejprve si implementujeme 2
pomocné funkce, které z popisu pomocí 3 délek stran vypočtou dva
různé úhly:
def get_alpha(a, b, c):
return acos(float(b ** 2 + c ** 2 - a ** 2) /
(2 * b * c)) * 180.0 / pi
def get_beta(a, b, c):
return acos(float(a ** 2 + c ** 2 - b ** 2) /
(2 * a * c)) * 180.0 / pi
Pro samotnou kontrolu funkcí z rodiny perimeter_* si definujeme
pomocnou proceduru, která pracuje s obecným trojúhelníkem, zadaným
délkami stran.
Na tomto místě si všimněte, že na číslech s plovoucí
desetinnou čárkou (typ float) nepoužíváme běžnou rovnost
==. Problém je, že výpočty tohoto typu mají omezenou
přesnost: vypočteme-li stejnou hodnotu (v matematickém
smyslu) dvěma různými postupy (označme výsledky jako x a
y), může sice platit x == y, ale stejně dobře může také
nastat x != y. To, co by mělo platit pokaždé je, že hodnoty
x a y jsou si „blízko“ – tzn. že, až na chybu způsobenou
nepřesností, jsou stejné. Žel, co přesně znamená „blízko“ není
přesně definované a záleží od konkrétního výpočtu. Nám bude
stačit výchozí definice funkce isclose z modulu math,
která funguje dobře ve většině situací.
assert isclose(perimeter_sss(a, b, c), a + b + c)
assert isclose(perimeter_sas(a, gamma, b), a + b + c)
assert isclose(perimeter_sas(b, alpha, c), a + b + c)
assert isclose(perimeter_sas(c, beta, a), a + b + c)
assert isclose(perimeter_asa(alpha, b, gamma), a + b + c)
assert isclose(perimeter_asa(beta, a, gamma), a + b + c)
assert isclose(perimeter_asa(alpha, c, beta), a + b + c)
Zbývá proceduru check_triangle zavolat na vhodně zvolené
trojúhelníky. Strany a a b můžeme volit libovolně:
def main(): # demo
for a in range(1, 6):
for b in range(1, 6):
stranu c pak ale musíme zvolit tak, aby byla splněna
trojúhelníková nerovnost (jinak budou funkce
perimeter_* zcela oprávněně počítat nesmysly):
for c in range(abs(a - b) + 1, a + b):
check_triangle(a, b, c)
Na závěr si ještě demonstrujeme případ, kdy je řešení
trojúhelníku skutečně nepřesné, totiž že výsledek, který
obdržíme různými způsoby, může být skutečně různý.
Napište predikát, který ověří, zda je číslo number palindrom,
zapíšeme-li jej v desítkové soustavě. Palindrom se vyznačuje tím,
že je stejný při čtení zleva i zprava.
Napište čistou funkci gcd, která pro zadaná kladná čísla nalezne
jejich největšího společného dělitele. Použijte naivní algoritmus
(tedy takový, který bude zkoušet všechny možnosti, počínaje
největším vhodným kandidátem).
Napište funkci count_digit_in_sequence, která spočte kolikrát se
cifra digit vyskytuje v číslech v rozmezí od čísla low po
číslo high včetně. Například cifra 1 se na intervalu od 0 po 13
vyskytuje šestkrát, konkrétně v číslech: 1 10 11 12 13.
Implementujte funkci power_digit_sum, která vrátí „speciální“
ciferný součet čísla number, který se od běžného ciferného
součtu liší tím, že každou cifru před přičtením umocníme na číslo
její pozice. Pozice číslujeme zleva, přičemž první má číslo 1.
Vstupem funkce power_digit_sum bude libovolné nezáporné celé
číslo, na výstupu se očekává celé číslo. Výpočet budeme provádět
v číselné soustavě se základem 7.
Příklad: Číslo zapíšeme v sedmičkové soustavě jako
– skutečně, . Proto power_digit_sum(1234) získáme jako .
Napište funkci, která vytvoří číslo zřetězením count po sobě
jdoucích kladných čísel počínaje zadaným číslem start. Tato
čísla zřetězte vyjádřená v binární soustavě. Například volání
joined(1, 3) zřetězí sekvenci , ,
a vrátí číslo . V Pythonu lze binární čísla přímo
zapisovat v tomto tvaru: 0b11011 (podobně lze stejné číslo
zapsat v šestnáctkové soustavě zápisem 0x1b nebo osmičkové jako
0o33).
V této úloze bude Vaším úkolem získat hodnotu index-tého
koeficientu řetězového zlomku pro racionální číslo s čitatelem
nom a jmenovatelem denom.
Řetězový zlomek je forma reprezentace čísla jako součet celého
čísla a převrácené hodnoty jiného čísla, které opět
reprezentujeme součtem celého čísla a další převrácené
hodnoty. Celá čísla postupně tvoří řadu koeficientů
řetězového zlomku.
Například řetězový zlomek
reprezentuje číslo a jeho koeficienty jsou 4, 2, 6 a 7.
Koeficienty řetězového zlomku pro číslo můžete získat
iterativním postupem:
Rozdělte číslo na jeho celočíselnou část a zlomkovou
část . Číslo přímo udává první koeficient posloupnosti,
tzn. , zbytek koeficientů je odvozen od (viz další krok).
Posloupnost má tedy tvar: .
Pro získání dalšího koeficientu opakujte 1. krok s převrácenou
hodnotou zlomkové části .
Napište funkci, která najde celé číslo x, které leží mezi
hodnotami low a high (včetně), a pro které vrátí funkce poly
maximální hodnotu (tzn. libovolné takové, že pro všechny
platí , kde je funkce, kterou počítá podprogram
poly).
def poly(x):
return 10 + 30 * x - 15 * x ** 3 + x ** 5
Napište predikát, který ověří, zda je číslo korektní číslo
platební karty. Číslo platební karty ověříte podle Luhnova
algoritmu:
zdvojnásobte hodnotu každé druhé cifry zprava; je-li výsledek
větší než 9, odečtěte od něj hodnotu 9,
sečtěte všechna takto získaná čísla a cifry na lichých
pozicích zprava (kromě první cifry zprava, která slouží jako
kontrolní součet),
číslo karty je platné právě tehdy, je-li po přičtení kontrolní
cifry celkový součet dělitelný 10.
Například pro číslo 28316 je kontrolní cifra 6 a součet je: . Po přičtení kontrolní
cifry je celkový součet . Protože je beze zbytku dělitelný
deseti, číslo karty je platné.
Napište funkci, která zjistí, kolik bude pracovních dnů v roce
year. Dny v týdnu mají hodnoty 0–6 počínaje pondělím s hodnotou 0.
Předpokládejte, že year je větší než 1600.
České státní svátky jsou:
datum
svátek
1.1.
Den obnovy samostatného českého státu
—
Velký pátek
—
Velikonoční pondělí
1.5.
Svátek práce
8.5.
Den vítězství
5.7.
Den slovanských věrozvěstů Cyrila a Metoděje
6.7.
Den upálení mistra Jana Husa
28.9.
Den české státnosti
28.10.
Den vzniku samostatného československého státu
17.11.
Den boje za svobodu a demokracii
24.12.
Štědrý den
25.12.
1. svátek vánoční
26.12.
2. svátek vánoční
Přestupné roky: v některých letech se na konec února přidává 29.
den. Jsou to roky, které jsou dělitelné čtyřmi, s výjimkou těch,
které jsou zároveň dělitelné 100 a nedělitelné 400.
Čistou funkci first_day můžete použít k tomu, abyste zjistili,
na který den v týdnu padne 1. leden daného roku. Např.
first_day(2001) vrátí nulu, protože rok 2001 začínal pondělím.
def first_day(year):
assert 1601 <= year
years = year - 1601
offset = years + years // 4 - years // 100 + years // 400
return offset % 7
Vaším úkolem bude spočítat, kolik následujících let Vám vydrží
úspory o hodnotě savings v bance. Na konci každého roku Vám
banka úročí obnos na účtu úrokovou sazbou interest_rate (zadanou
v procentech). Dále, abyste pokryli své životní náklady, na
začátku každého roku vyberete z účtu obnos withdraw, který se
každým rokem zvyšuje o inflaci inflation (opět zadanou
v procentech). Vybíraný obnos se po započítání inflace
zaokrouhluje dolů na celá čísla. Úroková sazba a inflace jsou
konstantní a meziročně se nemění. Po zúročení banka celkovou
částku zaokrouhluje dolů na celá čísla.
Příklad: při počátečním obnosu 100000 korun, ročních výdajích
42000 korun, úrokové sazbě 3,2 % a inflaci 1,5 % bude po prvním
roce na účtu . Další rok se výdaje
zvýší o inflaci na = 42630.
Budete-li mít hotovo, zkuste přemýšlet nad variantou, která by
se vyhnula použití aritmetiky s plovoucí desetinnou čárkou (tedy
s typem float). Budete si samozřejmě muset upravit zadání
i příložené testy – např. tak, že místo procent budou vstupem
promile (desetiny procent), ovšem zadaná celočíselně (tedy např.
15 místo 1.5).
Napište funkci, která spočítá počet pátků 13. v daném roce year.
Parametr day_of_week udává den v týdnu, na který v daném roce
padne 1. leden. Dny v týdnu mají hodnoty 0–6, počínaje pondělím
s hodnotou 0.
Přestupné roky: v některých letech se na konec února přidává 29.
den. Jsou to roky, které jsou dělitelné čtyřmi, s výjimkou těch,
které jsou zároveň dělitelné 100 a nedělitelné 400.
Napište funkci delete_to_maximal, která pro dané číslo number
najde největší možné číslo, které lze získat smazáním jedné
desítkové cifry.
def delete_to_maximal(number):
pass
Napište funkci delete_k_to_maximal, která pro dané číslo
number najde největší možné číslo, které lze získat smazáním
(vynecháním) k desítkových cifer.
Napište predikát is_visa, který je pravdivý, reprezentuje-li
číslo number platné číslo platební karty VISA, tj. začíná
cifrou 4, má 13, 16, nebo 19 cifer a zároveň je platným číslem
platební karty (viz příklad credit).
def is_visa(number):
pass
Dále napište predikát is_mastercard, který je pravdivý,
reprezentuje-li číslo number platné číslo platební karty
MasterCard, tj. začíná prefixem 50–55, nebo 22100–27209, má 16
cifer a zároveň je platným číslem platební karty.
† Napište funkci bisect, která aproximuje kořen spojité funkce
(předané parametrem fun) s chybou menší než epsilon na
zadaném intervalu od low po high včetně. Algoritmus bisekce
předpokládá, že v zadaném intervalu se nachází právě jedno řešení.
Při hledání řešení postupujte následovně:
spočtěte hodnotu funkce pro bod uprostřed intervalu, a je-li
výsledek v rozsahu povolené chyby, vraťte tento bod,
jinak spočtěte hodnoty funkce v hraničních bodech intervalu
a zjistěte, ve které polovině má funkce kořen,
opakujte výpočet s vybranou polovinou jako s novým intervalem.
Chybu spočtete v bodě jako .
Poznámka: funkci předanou parametrem můžete v Pythonu normálně
volat jako libovolnou jinou funkci.
Kladné celé číslo se nazývá -parazitní v soustavě o základu (kde
je celé číslo vetší než 1 a je celé číslo v rozsahu 1 až ),
pokud jeho -násobek vznikne tak, že jeho poslední (nejpravější) číslici
v zápisu v soustavě o základu přesuneme na první pozici. Například
číslo je 4-parazitní v desítkové soustavě, protože platí
; číslo je 2-parazitní v trojkové soustavě,
protože , a .
Napište čistou funkci is_parasitic, která zjistí, zda je zadané číslo
num-parazitní v soustavě o základu base pro nějaké – pokud ano,
takové vrátí; jinak vrátí None.
Elfové z Groglinky používají k zápisu čísel jedenáctkovou
soustavu, přičemž kromě nám známých číslic používají ještě číslici reprezentující hodnotu minus
jedna. (Tím se liší od ostatních elfů, kteří touto číslicí
reprezentují hodnotu deset). Napište čistou funkci
elf_digit_sum(num), která dostane na vstupu kladné celé číslo
a vrátí součet hodnot jeho číslic v zápise elfů z Groglinky.
Elfové používají k zápisu čísel jedenáctkovou soustavu, přičemž
kromě nám známých číslic používají
ještě číslici reprezentující číslo deset.
O kladném celém čísle řekneme, že je elfím palindromem, pokud se
jeho elfí (jedenáctkový) zápis čte stejně zleva i zprava poté, co
vynecháme všechny číslice a následně odstraníme zbytečné
levostranné nuly. (Za elfí palindromy považujeme i čísla, jejichž
elfí zápis je tvořen pouze číslicemi .)
Napište predikát elf_palindrome(num), který vrátí True, je-li
zadané číslo elfím palindromem; False jinak.
Například číslo je elfím palindromem, protože jeho elfí
zápis je . Elfími palindromy jsou také čísla , a .
Elfími palindromy nejsou čísla , .
Cvelfové používají k zápisu čísel dvanáctkovou soustavu, přičemž
kromě nám známých číslic používají
ještě číslice (s hodnotou deset) a (s hodnotou jedenáct).
Cvelfí míchání je taková operace, kdy vezmeme kladné celé číslo
v cvelfím zápisu a přeskládáme jeho číslice tak, aby všechny
číslice stály vlevo a všechny číslice stály vpravo.
Ostatní číslice zůstanou v původním pořadí. Výsledný zápis pak
opět přečteme jako číslo v cvelfím zápisu. Napište čistou funkci
zwelf_shuffle(num), která dostane na vstupu kladné celé číslo
a vrátí výsledek po cvelfím míchání.
Například:
číslo 3302 zapíše cvelf jako a po cvelfím míchání z něj
vznikne ,
číslo 1587 zapíše cvelf jako a po míchání z něj vznikne
(levostrannou nulu při čtení zápisu samozřejmě
ignorujeme),
číslo 1729 zapíše cvelf jako a to se tedy mícháním nezmění.
Tento týden se budeme poprvé zabývat složenými datovými typy,
konkrétně těmi, které reprezentují sekvence: seznamy a uspořádanými
n-ticemi. Prozatím jsme se setkali pouze s hodnotami tzv.
skalárních typů: zejména int, float, bool. Použití těchto
datových typů nám umožňovalo pamatovat si fixní množství dat:
například při výpočtu -tého prvku Fibonacciho posloupnosti jsme
si potřebovali pamatovat tři čísla, které jsme měli uložené ve třech
proměnných. To, co nám ale hodnoty tohoto charakteru neumožňovaly,
bylo například zapamatovat si všechny dosud spočtené prvky. Zkuste
se zamyslet, co by se stalo, kdybychom chtěli vyčíslit -tý prvek
posloupnosti zadané třeba takto (v OEIS nalezne pod číslem A165552):
kde když dělí a 0 jinak. Tady už nestačí
pamatovat si poslední dva prvky – co je horší, nestačí nám žádný
konstantní počet proměnných: potřebujeme jich tolik, kolikátý prvek
chceme spočítat.
To je přesně situace, kdy lze použít sekvenční datový typ: hodnota
sekvenčního typu se skládá z libovolného počtu jiných hodnot,
očíslovaných po sobě jdoucími celými čísly. Číslu, které popisuje
pozici „vnitřní“ hodnoty, říkáme index, a podobně jak tomu bylo
s indexovými proměnnými, první prvek má číslo (index) 0.
V Pythonu existují dva základní sekvenční typy: první je
uspořádaná n-tice (anglicky tuple, případně n-tuple), ten
druhý pak seznam (anglicky list). Hodnoty obou těchto typů mají
vnitřní strukturu – vzpomeňte si, že proměnné váží hodnoty ke
jménům: sekvence obdobně váže hodnoty k indexům (celým číslům).
Seznam a n-tice se tedy chovají podobně, jako bychom měli proměnné
pojmenované lst[0], lst[1], lst[2], atd. K těmto pomyslným
proměnným můžeme navíc přistupovat nepřímo: jako index můžeme
použít nejen konstantu, ale libovolné jiné číslo v programu – klidně
třeba hodnotu proměnné, nebo i výraz, např. lst[i] nebo lst[i +
1].
Obdoba použití proměnné (např. ve výrazu x + 1, který se
vyhodnotí na 5) je indexace seznamu, např. lst[0] se
vyhodnotí na 1, lst[2] + 1 se vyhodnotí na 3, atp. Výraz
lst[x] se vyhodnotí na 5.
Máme-li hodnotu typu seznam, můžeme navíc měnit na kterou hodnotu
ten-který index odkazuje, a tato změna odkazu je zcela analogická
přiřazení do proměnné. Toto vnitřní přiřazení zapisujeme
podobně jako to běžné, např. lst[3] = 9, a má obdobný efekt (na
obrázku je již pouze hodnota typu seznam z proměnné lst):
Uspořádaná n-tice se pak od seznamu liší zejména tím, že nemá
vnitřní přiřazení: přiřazení hodnot indexům je tedy pevně dané při
vytvoření n-tice a nelze jej již dále v programu měnit. Zároveň do
n-tice nelze po jejím vzniku přidávat nové indexy (těm by totiž bylo
potřeba přiřadit hodnoty, a to v n-tici nelze).
Použití seznamů a n-tic si dále demonstrujeme na několika ukázkách:
statistics – iterace a indexace seznamů
fibonacci – konstrukce nového seznamu
sequence – výpočet výše uvedené posloupnosti
points – práce s n-ticemi a seznamy n-tic
rotate – mutace (vnitřní přiřazení) na seznamech
Elementární příklady:
predicates – predikáty na seznamech
explosion – filtrování seznamu podle kritéria
cartesian – výpočet kartézského součinu
Přípravy:
numbers – převod číselných soustav
fraction – vyhodnocení řetězového zlomku
histogram – četnost hodnot ve vstupním seznamu
length – délka lomené čáry
merge – sloučení dvou uspořádaných seznamů
cellular – jednoduché buněčné automaty
Rozšířené úlohy:
quiz – vyhodnocení multiple-choice testu
rectangles – překryv obdélníků v zadaném seznamu
concat – spojování vnořených seznamů
rcellular – buněčný automat in situ
squares – metoda nejmenších čtverců
partition † – přerozdělení seznamu podle velikosti
Volitelné úlohy:
flats – hledání rovin ve dvourozměrném terénu
plateau – náhorní plošiny v podobném duchu
exponent – výběr čísla podle prvočíselného rozkladu
Tato kapitola přidává do našeho jazyka důležité prostředky pro popis
a práci se složenými datovými typy (doteď jsme pracovali pouze
s čísly a logickými hodnotami). Protože složená data jsou hodnoty,
podobně jako čísla, většina změn se bude týkat výrazů. Mezi
příkazy se objeví nová varianta cyklu for (pro procházení seznamu)
a nové varianty přiřazení.
Literály jsou typem výrazů. V této kapitole se objeví dva typy
literálů: seznamový literál a literál n-tice.
Seznamový literál má tvar [výraz₁, výraz₂, …, výrazₙ] (výrazy
oddělené čárkami, uzavřené do hranatých závorek) a jeho významem je
seznam, který má na indexu hodnotu, která vznikla vyhodnocením
výrazu výrazᵢ. Výrazů může být libovolný počet, včetně nuly
(v takovém případě má výraz podobu [] a jeho hodnotou je prázdný
seznam). Příklady:
Podobně, ale s kulatými závorkami, zapisujeme literál n-tice; ten
má 3 možné podoby:
() označuje prázdnou n-tici,
(výraz,) označuje 1-tici (všimněte si koncové čárky),
(výraz₁, výraz₂, …, výrazₙ) pro .
Význam je analogický jako v případě seznamu. V některých případech
lze kulaté závorky v zápisu n-tice vynechat, je-li takový zápis
jednoznačný (podobně jako lze vynechat některé závorky
v aritmetických výrazech). Můžeme tedy psát např. (vpravo
ekvivalentní zápis s vypsanými závorkami):
return 1, 2 ⋅ return (1, 2)
x = 7, a + 1 ⋅ x = (7, a + 1)
a = x + 1, f(3), 7 ⋅ a = (x + 1, f(3), 7)
Ve všech uvedených případech jsou čárkami oddělené hodnoty
interpretovány jako n-tice. Tuto zkratku ale nelze použít např.
v parametru podprogramu nebo v seznamovém literálu.
Pro práci s n-ticemi budeme často používat tzv. rozbalení. Nejedná
se ani o výraz ani o příkaz: je to speciální zápis, který se může
objevit na levé straně přiřazení, v cyklu for a
v intenzionálních seznamech. Zápisem se podobá na literál n-tice,
ale místo výrazů obsahuje jména: (jméno₁, jméno₂, …, jménoₙ).
Podobně jako v literálu lze kulaté závorky vynechat. Můžeme tedy
psát např.:
(x, y) = (1, 2)
x, y = (1, 2)
x, y = 1, 2
x, y = point_2d
x, y, z = point_3d
x, y = y, x
Pro práci se seznamy se nám budou hodit dvě nové varianty cyklu
for; první z nich (základní) zapisujeme:
for vazby in seznam:
příkazy
kde se výrazseznam vyhodnotí na seznam a vazby je buď
jméno nebo rozbalení. Tělo cyklu (příkazy) se pak provede
jednou pro každý prvek seznamu seznam. V -té iteraci odpovídají
vazby-tému prvku seznamu seznam. Je-li seznam prázdný,
tělo se neprovede ani jednou.
Rozšířená verze
for index, vazby in enumerate(seznam):
příkazy
má stejný význam jako v předchozím případě, s těmito změnami:
index je jméno, které váže index právě iterovaného prvku
v seznamu seznam (nebo ekvivalentně váže pořadové číslo právě
prováděné iterace, počítáno od 0),
v případě, kdy jsou vazbyrozbalení, musí být uzavřeny
v kulatých závorkách (jinými slovy, na tomto místě nelze závorky
vynechat).
Dále přidáme dvě nové varianty příkazu přiřazení:
na levé straně se může krom jména objevit také výše popsané
rozbalení: jméno₁, …, jménoₙ = výraz s významem analogickým
běžnému přiřazení (pouze je dotčeno několik proměnných najednou),
o něco komplikovanější je přiřazení do prvku seznamu, které
zapisujeme jako seznam[index] = výraz kde seznam je jméno a
index je výraz s celočíselnou hodnotou.
Přiřazení do prvku seznamu (nazýváme ho též vnitřním přiřazením)
se ale svým významem od běžného přiřazení podstatě odlišuje: tento
příkaz upraví stávající objekt, který je přiřazen jménu seznam.
Krom literálů přibývá se složenými datovými typy ještě několik
nových výrazů. Prvním z nich je indexace, která má tvar
seznam[index], kde:
seznam je jméno proměnné (typu seznam),
index je aritmetický výraz (jeho hodnotou je celé číslo),
výsledkem je hodnota, která je v seznamu jméno uložena na
indexu , kde je hodnota, na kterou se vyhodnotil výraz
index.
Například:
a[0]
numbers[i + 1]
names[compute_index(m, n)]
Dalším novým typem výrazu je použití (volání) metody, které má
tvar objekt.metoda(výraz₁, …, výrazₙ) a je obdobou použití
podprogramu (volání funkce), který je ve speciálním vztahu
s objektem vázaným ke jménuobjekt:
nejprve se vyhodnotí parametry výraz₁, …, výrazₙ,
provede se volání samotné metody s názvem metoda,
hodnotou výrazu je návratová hodnota volané metody.
Další dva nové typy výrazů nám umožní zapisovat hodnoty typu seznam:
seznamový literál, který jsme již zavedli výše, nám umožňuje
zapsat seznam o pevném počtu prvků, a
intenzionální seznam, kterého délka může být proměnlivá, ale
uložené hodnoty se řídí nějakým předpisem.
Intenzionální seznam má tyto tvary:
[prvek for jméno in range(počet)], kde
počet je výraz s celočíselnou hodnotou,
výsledkem je seznam, který má počet prvků,
prvek vznikne vyhodnocením výrazu prvek, přičemž jméno
má pro dané vyhodnocení hodnotu (počínaje nulou),
[prvek for jméno in range(n₁, n₂)], je analogický, ale hodnoty
vázané na jméno jsou z intervalu ,
[prvek for jméno in rozsah if podmínka], kde rozsah je
range(počet) nebo range(od, do) a který má stejný význam jako
předchozí, ale obsahuje pouze ty prvky, pro které se podmínka
vyhodnotí jako pravdivá,
[prvek for vazby in seznam], kde
seznam je výraz typu seznam,
vazby jsou rozbalení nebo jméno,
hodnotou je seznam, který má stejný počet prvků jako seznam a
na -té pozici je hodnota, která vznikne vyhodnocením
výrazuprvek, přičemž vazby v každém vyhodnocení
odpovídají -tému prvku seznamu seznam,
[prvek for vazby in seznam if podmínka], který je
opět ekvivalentní předchozímu, ale opět obsahuje pouze ty prvky,
pro které se podmínka vyhodnotí jako pravdivá.
Výrazy podmínka se v obou případech vyhodnocují se stejnými
vazbami, jako výraz prvek. Příklady:
[1 for i in range(5)]
[i + 1 for i in range(2 * count)]
[2 * i for i in range(7) if i != 3]
[2 * i for i in numbers]
[i ** 2 for i in numbers if i > 0]
Poslední nový typ výrazu je obměnou již známých relačních operátorů:
výrazy x == y, x != y, x < y, x > y, x >= y, x <= y
připouštíme i v případech, kdy se oba podvýrazy x, y vyhodnotí
na seznamy, nebo se oba vyhodnotí na n-tice. Operátor < je v tomto
případě dán lexikografickým uspořádáním:
je-li x prefixem y nebo naopak, jako menší se vyhodnotí
hodnota s menším počtem prvků,
jinak nechť je nejmenší index, na kterém se x a y liší a
xᵢ a yᵢ jsou prvky na této pozici; výraz x < y se vyhodnotí
na výsledek srovnání xᵢ < yᵢ.
Chování ostatních operátorů je již jednoznačně určeno rovností a
operátorem <.
Tato ukázka demonstruje základní použití seznamů: zejména jejich
indexaci a iteraci. Oba tyto koncepty si demonstrujeme na
výpočtu jednoduchých statistik nad prvky předem daného seznamu:
průměru, mediánu a směrodatné odchylky.
Jako první statistiku vypočteme průměr, který získáme jako podíl
součtu všech prvků vstupního seznamu a jeho délky. Protože obě
tyto operace jsou v Pythonu zabudované, je definice velice
jednoduchá:
Protože indexace je v určitém smyslu jednodušší než iterace,
budeme pokračovat výpočtem mediánu: medián je hodnota, která se
objeví v uspořádaném souboru čísel uprostřed. Protože zatím
neumíme posloupnosti řadit, budeme požadovat, by vstupem byla
posloupnost již seřazená. Tuto posloupnost budeme reprezentovat
neprázdným seznamem:
def median(data):
Zbývá tedy vypočítat index, na kterém nalezneme medián: tady
nastávají dvě možnosti: buď je seznam liché, nebo sudé délky.
Délku seznamu zjistíme vestavěnou (čistou) funkcí len:
if len(data) % 2 == 1:
Případ liché délky je jednodušší, proto jej vyřešíme
první. V tomto případě existuje skutečný prostřední prvek,
a my pouze vrátíme jeho hodnotu. Celočíselné dělení dvěma
nám dá právě ten správný index – přesvědčte se o tom!
return data[len(data) // 2]
else:
V opačném případě je seznam sudé délky (prázdný seznam
neuvažujeme, nevyhovuje vstupní podmínce). Běžná definice
mediánu v tomto případě říká, že výsledkem má být
aritmetický průměr obou „prostředních“ hodnot (těch, které
jsou nejblíže pomyslnému středu, který se nachází přesně
mezi nimi).
Poslední a nejsložitější statistikou je tzv. směrodatná odchylka. Tuto spočítáme jako odmocninu tzv. rozptylu, který je
popsaný následovným vztahem ( je počet prvků, jsou
jednotlivé prvky a je průměr):
def stddev(data):
Pro výpočet jednotlivých členů budeme potřebovat průměr,
který již máme implementovaný výše. Dále si nachystáme
střadač (akumulátor), do kterého sečteme jednotlivé
kvadratické odchylky :
mean = average(data)
square_error_sum = 0.0
Chceme-li pro každý prvek seznamu provést nějakou akci nebo
výpočet, použijeme k tomu cyklus. Mohli bychom samozřejmě
použít konstrukce, které již známe: indexovou proměnnou,
cyklus tvaru for i in range(n), funkci len a indexaci
seznamu data. V případě, že ale indexovou proměnnou
nepotřebujeme k ničemu jinému, než indexaci jednoho seznamu,
lze použít mnohem úspornější a čitelnější zápis:
for x_i in data:
V těle takovéhoto cyklu máme v proměnné x_i uloženy
přímo hodnoty ze seznamu data, nemusíme tedy vůbec
indexovat.
square_error_sum += (x_i - mean) ** 2
Protože rozptyl (variance) je vlastně střední (průměrná)
kvadratická odchylka s drobnou korekcí, vypočteme…
variance = square_error_sum / (len(data) - 1)
… a celkový výsledek získáme jako odmocninu rozptylu:
return sqrt(variance)
Konečně funkčnost ověříme na několika jednoduchých příkladech.
V této ukázce si demonstrujeme vytváření seznamu, který bude
výstupem (čisté) funkce fib. Seznam bude obsahovat prvních n
členů Fibonacciho posloupnosti, které vypočteme už známým postupem
(viz též fibonacci.py z části 1).
def fib(n):
Seznam budeme budovat v cyklu. Proměnné a a b již nebudeme
potřebovat, protože máme k dispozici celý seznam, bylo by tedy
nehospodárné pamatovat si dva prvky ještě jednou, a to jak z
pohledu využití paměti (i když v tomto případě by to nebyl
velký prohřešek), ale zejména z pohledu čitelnosti programu.
Většinou je nežádoucí uchovávat stejnou informaci na více
místech, není potom často jasné, jsou-li obě „místa“ plně
ekvivalentní, a pokud ano, tak že se chybou v programu nemůžou
rozejít.
out = [1, 1]
for i in range(n - 2):
Pro výpočet dalšího Fibonacciho čísla využijeme zápis pro
indexování seznamu od konce: je-li použitý index záporný,
automaticky se k němu přičte délka indexovaného seznamu,
tzn. out[-2] je totéž jako out[len(out) - 2].
Rozmyslete si, že tento výraz skutečně popisuje
předposlední prvek seznamu out!
value = out[-1] + out[-2]
Přidání na konec existujícího seznamu provedeme voláním
metodyappend. Metody jsou podprogramy, které často
leží někde mezi procedurou a čistou funkcí (nicméně i
metody můžou být čisté, a naopak můžou mít i charakter
procedury). Mají navíc ale jednu speciální vlastnost,
v podobě význačného parametru, který píšeme při
volání před jejich jméno. Následovné volání append má
tedy dva parametry – out a value.
out.append(value)
Nyní stojíme před drobným problémem: mohlo se stát, že
volající si vyžádal méně než dva prvky posloupnosti, ale my
jsme pro pohodlí výpočtu do seznamu vložili první dvě hodnoty.
Jedna možnost řešení byla hned na začátku funkce ověřit, zda
není n nula nebo jedna, a rovnou vrátit příslušný seznam
([] nebo [1]). My tento problém místo toho využijeme,
abychom si ukázali, jak ze stávajícího seznamu hodnoty navíc
odstranit. Rozmyslete si, že tělo cyklu se provede skutečně
právě jednou, je-li n = 1 a dvakrát, je-li n = 0. Metoda
pop (bez dalších parametrů) odstraní ze seznamu poslední
prvek.
while len(out) > n:
out.pop()
return out
Jako obvykle, program zakončíme několika testy, abychom se
ujistili, že námi implementovaná funkce pracuje (aspoň v některých
případech) správně.
V předchozích dvou ukázkách byl seznam vstupem nebo výstupem
funkce. Nyní se podíváme na funkci, která má na vstupu i výstupu
pouze jediné číslo, ale seznam využije pro svůj výpočet. Vrátíme
se k výpočtu n-tého prvku posloupnosti, podobně jak tomu bylo
v příkladech z první části. Vyčíslovat budeme posloupnost, se
kterou jsme se setkali v úvodu:
kde když dělí a 0 jinak. Implementace bude
formou čisté funkce.
def sequence(position):
Podobně jako při výpočtu fib v předchozí ukázce si vytvoříme
proměnnou, ve které budeme mít uložen dosud vypočtený prefix
posloupnosti. V tomto případě to ale není proto, abychom jej
mohli použít jako návratovou hodnotu, ale čistě pro naše
interní účely.
seq = [1]
Do seznamu seq budeme v cyklu přidávat nové prvky
posloupnosti, v každé iteraci jeden. Potřebujeme provést n -
1 iterací (jeden prvek už v seznamu máme). Nabízí se dvě
možnosti: for cyklus, podobně jako v předchozím, nebo
while cyklus. Protože potřebujeme indexovat od 1, není for
cyklus příliš pohodlný, navíc u while cyklu je na pohled
zřejmé, že má správný počet iterací, přikloníme se k této
variantě:
while len(seq) < position:
Do proměnné n si uložíme index právě počítaného prvku
(číslováno od 1).
n = len(seq) + 1
Nyní potřebujeme vypočítat hodnotu, kterou přidáme na
konec seznamu. Nachystáme si střadač total, ve kterém
budeme počítat definiční sumu, a indexovou proměnnou k
(která bude indexovat už vypočtené hodnoty počínaje první
s indexem 1).
total = 0
k = 1
Samotný výpočet sumy provedeme opět v cyklu.
while k < n:
if n % k == 0:
total += k * seq[k - 1]
k += 1
V proměnné total máme nyní další prvek posloupnosti, který
si přidáme do seznamu seq a pokračujeme další iterací.
seq.append(total)
Seznam seq byl čistě pomocný – umožnil nám provést výpočet.
Výsledkem funkce je ale jediné číslo, totiž position-tý
prvek posloupnosti. Ten nalezneme na indexu position - 1
(seznamy indexujeme od nuly, první prvek je tedy na indexu 0,
atd.).
Uspořádané n-tice jsou v Pythonu velmi podobné seznamům: lze je
indexovat a iterovat, ptát se na jejich délku funkcí len, ale
také například vytvářet (n+m)-tice spojením n-tice s m-ticí. Jak
jsme již zmiňovali v úvodu, zásadní rozdíl je, že n-tice nemá
vnitřní přiřazení a nelze ji tedy po vytvoření měnit.
Ve skutečnosti ale n-tice používáme v programech výrazně jinak
než seznamy, přestože mají velmi podobnou strukturu a operace.
V typickém použití obsahuje seznam pouze jeden typ hodnot, ale
počet hodnot je variabilní. N-tice se chovají opačně: je běžné, že
obsahují hodnoty různých typů (ale všechny n-tice daného určení
mají na stejném indexu stejný typ) a mají fixní počet položek.
Tento princip si demonstrujeme na příkladu, kde budeme pracovat
s barevnými body v rovině. Body budeme reprezentovat jako trojice
(souřadnice x, souřadnice y, barva). Každá n-tice, která
reprezentuje bod, bude mít právě tuto strukturu, a bude mít vždy
3 složky (budeme tedy mluvit o trojicích). Navíc bude platit, že
první dvě složky budou vždy čísla, a třetí složka bude vždy
řetězec.
V principu můžeme k těmto složkám přistupovat indexací, ale
existuje i mnohem lepší zápis – rozbalení n-tice do proměnných.
Srovnejte si zápis x, y, colour = point, kde dále pracujeme se
jmény x, y a colour, oproti point[0] a point[1] pro
souřadnice a point[2] pro barvu. Pro srovnání si můžete v tomto
příkladu přepsat všechny rozbalení trojic na indexaci a zvážit, co
se Vám lépe čte.
Jako první si definujeme jednoduchou (čistou) funkci, která spočte
Euklidovskou vzdálenost dvou bodů (která samozřejmě nezávisí na
jejich barvě).
Poznámka: použití _ jako názvu proměnné není z pohledu Pythonu
ničím zvláštním, jedná se o identifikátor jako kterýkoliv jiný.
Nicméně jeho použitím indikujeme budoucím čtenářům, že hodnotu
této proměnné nehodláme používat, a domluvou se tedy jedná
o zástupný symbol.
Dále si definujeme funkci, která v neprázdném seznamu najde barvu
„nejlevějšího“ bodu (takového, který má nejmenší x-ovou
souřadnici).
def leftmost_colour(points):
x_min, _, result = points[0]
for x, _, colour in points:
if x < x_min:
x_min = x
result = colour
return result
Dále si definujeme čistou funkci, která dostane jako parametry
seznam bodů points a barvu colour, a jejím výsledkem bude bod,
který se nachází v těžišti soustavy bodů dané barvy (a který
bude stejné barvy). Vstupní podmínkou je, že points obsahuje
aspoň jeden bod barvy colour.
Jako poslední si definujeme (opět čistou) funkci, která spočítá
průměrnou vzdálenost bodů různé barvy. Vstupní podmínkou je, že
seznam points musí obsahovat aspoň dva různobarevné body.
def average_nonmatching_distance(points):
total = 0.0
pairs = 0
for i in range(len(points)):
for j in range(i):
_, _, i_colour = points[i]
_, _, j_colour = points[j]
if i_colour != j_colour:
total += distance(points[i], points[j])
pairs += 1
return total / pairs
Testy jsou tentokrát rozsáhlejší, protože jsme definovali větší
počet funkcí. Pro úsporu horizontálního místa některé testy
používají lokální aliasy pro funkce, např. dist =
average_nonmatching_distance – takové přiřazení znamená, že
dist je (lokální) synonymum pro average_nonmatching_distance.
V poslední ukázce pro tento týden se budeme zabývat vnitřním
přiřazením, tzn. změnou samotné hodnoty typu seznam (změnou
vnitřních vazeb indexů na hodnoty). Po delší době tedy budeme
implementovat proceduru (podprogram, kterého hlavním smyslem je
provést nějakou akci – v tomto případě pozměnit existující
hodnotu). Tato procedura provede rotaci seznamu (na místě)
o zadaný počet prvků. Např. rotací seznamu [1, 2, 3, 4]:
o jedna doprava dostaneme seznam [4, 1, 2, 3],
o dva doprava seznam [3, 4, 1, 2],
o dva doleva tentýž seznam [3, 4, 1, 2] a konečně,
o jedna doleva seznam [2, 3, 4, 1].
Směr rotace určíme dle znaménka: kladná čísla budou rotovat
doprava, záporná doleva.
Možností, jak „in situ“ rotaci seznamu implementovat je několik,
my si ukážeme dvě. První je konceptuálně nejjednodušší, ale
nepříliš efektivní: jako základní operaci používá posuv o jedna
doleva nebo doprava. Každá rotace o jedničku musí projít celý
seznam, posuvy o větší počet prvků budou tedy procházet celý
seznam mnohokrát – proto je tato implementace neefektivní.
def rotate_naive(lst, amount):
while amount != 0:
if amount < 0:
Posuv doleva implementujeme tak, že první prvek
přesuneme na poslední místo a všechny ostatní o jedna
doleva.
backup = lst[0]
for i in range(len(lst) - 1):
lst[i] = lst[i + 1]
lst[-1] = backup
amount += 1
else:
Posuv doprava je analogický, ale všechny přesuny budou
opačným směrem.
backup = lst[-1]
for i in range(len(lst) - 1, 0, -1):
lst[i] = lst[i - 1]
lst[0] = backup
amount -= 1
Jiná možnost je prvky rovnou posouvat na správné místo v seznamu
(použitím vnitřního přiřazení), musíme si ale pamatovat prvky,
které takto přepisujeme, a to až do doby, než je můžeme samotné
přesunout na jejich cílovou pozici. Takových prvků může být
najednou až tolik, jaká je velikost posuvu. Každý prvek ale
přesouváme nejvýše jednou (bez ohledu na velikost posuvu), celkový
počet operací je tedy výrazně menší než v předchozí implementaci.
def rotate_smart(lst, amount):
Pro jednoduchost implementujeme pouze posuvy doprava – posuvy
doleva by byly analogické. Díky tomu je tato implementace při
rotacích doleva méně efektivní (malé otočení doleva je totéž
jako velké otočení doprava). V proměnné backup si budeme
pamatovat ty prvky, které budeme v nejbližší době ukládat na
své cílové pozice (po prvních amount přesunech zde budou
uloženy právě ty prvky, které aktuálně v lst dočasně chybí).
amount = amount % len(lst)
backup = []
for i in range(0, amount):
backup.append(lst[i])
for i in range(len(lst)):
Do target spočteme cílové políčko pro další přesun, a
prvek zde umístěný prohodíme s příslušným prvkem v seznamu
backup. Na pozici i % amount seznamu backup se
nachází prvek, který byl v původním seznamu na pozici i,
a tedy je to ten prvek, který potřebujeme umístit do
lst[target]. Jejich prohozením se do backup[i %
amount] dostane prvek, který byl v původním seznamu na
pozici target (tj. i + amount) a tedy se k němu
vrátíme po dalších amount iteracích ((i + amount) %
amount == i % amount).
Protože máme dvě implementace stejné funkce, testy si
parametrizujeme konkrétní implementací, aby nám stačilo napsat je
jednou. Za parametr rotate se postupně doplní rotate_naive a
rotate_smart.
Napište (čistou) funkci survivors, ktorá ze vstupního seznamu
objects spočítá nový seznam, který bude obsahovat všechny prvky
z objects, které jsou dostatečně vzdálené (dále než radius) od
bodu center.
Můžete si představit, že funkce implementuje herní mechaniku, kdy
v bodě center nastala exploze tvaru koule, která zničila vše
uvnitř poloměru radius, a funkce survivors vrátí všechny
objekty, které explozi přežily.
Prvky parametru objects a parametr center jsou uspořádané
trojice, které reprezentují body v prostoru.
V této úloze naprogramujeme trojici (čistých) funkcí, které slouží
pro práci s číselnými soustavami. Reprezentaci čísla v nějaké
číselné soustavě budeme ukládat jako dvojici (base, digits), kde
base je hodnota typu int, která reprezentuje základ soustavy,
a digits je seznam cifer v této soustavě, kde každý prvek je
hodnota typu int, která spadá do rozsahu [0, base - 1]. Index
seznamu digits odpovídá příslušné mocnině base. Například:
(10, [2, 9]) je zápis v desítkové soustavě a interpretujeme
jej jako 2 * 1 + 9 * 10, co odpovídá číslu 92
(7, [2, 1]) je zápis v sedmičkové soustavě a kóduje
2 * 1 + 1 * 7 = 9
První funkce implementuje převod čísla number do ciferné
reprezentace v soustavě se základem base:
def to_digits(number, base):
pass
Další funkce provádí převod opačným směrem, z ciferné
reprezentace number vytvoří hodnotu typu int:
def from_digits(number):
pass
Konečně funkce convert_digits převede ciferný zápis z jedné
soustavy do jiné soustavy. Nápověda: tato funkce je velmi
jednoduchá.
Stejně jako v 02/fraction.py budete v této úloze pracovat s řetězovým
zlomkem. Tentokrát implementujeme převod opačným směrem, na vstupu bude
seznam koeficientů řetězového zlomku, a výstupem bude zlomek klasický.
Naprogramujte tedy čistou funkci continued_fraction, která dostane jako
parametr seznam koeficientů a vrátí zlomek ve tvaru (numerator,
denominator).
Napište (čistou) funkci, která pro zadaný seznam nezáporných čísel
data vrátí nový seznam obsahující dvojice – číslo a jeho
četnost. Výstupní seznam musí být seřazený vzestupně dle první
složky. Můžete předpokládat, že v data se nachází pouze celá
čísla z rozsahu [0, 100] (včetně).
Napište čistou funkci, která dostane na vstup seznam bodů v rovině
(tj. seznam dvojic čísel) a vrátí délku lomené čáry, která těmito
body prochází (tzn. takové, která vznikne spojením každých dvou
sousedních bodů seznamu úsečkou). Souřadnice i délky
reprezentujeme čísly s plovoucí desetinnou čárkou (typ float).
Například seznam [(0, 0), (1, 0), (1, 1), (2, 1)] definuje tuto
lomenou čáru:
složenou ze tří segmentů (úseček) velikosti 1. Její délka je 3.
Naprogramujte (čistou) funkci, která ze dvou vzestupně seřazených
seznamů čísel a, b vytvoří nový vzestupně seřazený seznam,
který bude obsahovat všechny prvky z a i b. Nezapomeňte, že
nesmíte modifikovat vstupní seznamy (jinak by funkce nebyla
čistá). Pokuste se funkci naprogramovat efektivně.
Napište (čistou) funkci, která simuluje jeden krok výpočtu
jednorozměrného buněčného automatu (cellular automaton). My se
omezíme na binární (buňky nabývají hodnot 0 a 1) jednorozměrný
automat s konečným stavem: stav takového automatu je seznam
jedniček a nul, například:
Protože obecný automat tohoto typu je stále relativně složitý,
budeme implementovat automat s fixní sadou pravidel:
old[i - 1]
old[i]
old[i + 1]
new[i]
0
0
1
1
1
0
0
1
1
0
1
1
1
1
0
0
1
1
1
0
Pravidla určují, jakou hodnotu bude mít buňka v následujícím
stavu, v závislosti na několika okolních buňkách stavu nynějšího
(konkrétní indexy viz tabulka). Neexistuje-li pro danou vstupní
kombinaci pravidlo, do nového stavu přepíšeme stávající hodnotu
buňky. Na krajích stavu interpretujeme chybějící políčko vždy
jako nulu.
Výpočet s touto sadou pravidel tedy funguje takto:
Na vstupu dostanete stav (konfiguraci) state, výstupem funkce je
nový seznam, který obsahuje stav vzniklý aplikací výše uvedených
pravidel na state.
Naprogramujte funkci mark_points, která spočítá počet bodů,
které student získal v multiple-choice testu. Vypracované řešení
je reprezentováno parametrem solution, kterého prvky odpovídají
možnostem, které student označil (tzn. je-li solution[0] rovno
2, odpověď na první otázku byla 2). Správné odpovědi jsou
v parametru answers jako seznam dvojic, kde pozice v seznamu
odpovídá číslu otázky, a dvojice je ve formě (správná odpověď,
body).
Napište (čistou) funkci, která jako parametr dostane seznam
obdélníků a vrátí seznam obdélníků, které se překrývají s nějakým
jiným. Obdélník samotný je reprezentovaný dvěma body (levým dolním
a pravým horním rohem, a má nenulovou výšku i šířku). Obdélníky
budeme zapisovat jako dvojice dvojic – ((0, 0), (1, 2))
například reprezentuje tento obdélník:
Mohl by se Vám hodit predikát, který je pravdivý, když se dva
obdélníky překrývají:
Podobně jako v cellular budeme v této úloze pracovat s 1D
buněčným automatem. Místo výpočtu nové konfigurace do nového
seznamu ale budeme modifikovat stávající seznam.
Toto samozřejmě nelze při použití stejných pravidel: v době
vyhodnocování i-té buňky by již byla buňka s indexem i - 1
přepsaná novou hodnotou. Proto použijeme pravidlo, které se dívá
jen doprava:
old[i]
old[i + 1]
old[i + 2]
new[i]
1
0
0
0
0
1
0
1
0
1
1
1
1
0
1
0
1
1
1
0
Opět platí, že není-li nějaká konfigurace v tabulce uvedena,
hodnota na indexu i se nemění.
Na rozdíl od předchozích příkladů, budeme v tomto implementovat
proceduru: cellular_in_situ nebude hodnotu vracet, místo toho
bude editovat seznam, který dostala jako parametr (viz též úvod
k tomuto týdnu).
Napište čistou funkci least_squares, která dostane na vstupu dva
stejně dlouhé seznamy čísel. Hodnoty na odpovídajících pozicích
v těchto seznamech udávají souřadnice jednoho vstupního bodu.
Výsledkem funkce nechť je trojice kde
udává přímku, která nejlépe aproximuje zadané body, a je seznam
tzv. residuí (vertikálních vzdáleností jednotlivých bodů od
vypočtené přímky). Označíme-li souřadnice jednotlivých bodů a , aritmetické průměry příslušných seznamů,
hledané koeficienty získáte použitím těchto vzorců:
V případě, že body leží na vertikální přímce (a tedy není
definovaná), vraťte místo trojice hodnotu None.
† Naprogramujte proceduru partition, která na vstup dostane
seznam čísel data a platný index idx. Pro pohodlnost hodnotu
data[idx] nazveme pivot.
Procedura přeuspořádá seznam tak, že přesune prvky menší než
pivot před pivot a prvky větší než pivot za pivot.
Po transformaci bude tedy seznam pomyslně rozdělen na tři části:
čísla menší než pivot
pivot
čísla větší než pivot
Relativní pořadí prvků v první a poslední části není definováno,
takže oba následovné výsledky pro seznam [3, 4, 1, 2, 0] a index
0 jsou správné: [1, 0, 2, 3, 4] nebo [1, 2, 0, 3, 4].
Mějme seznam nezáporných celých čísel reprezentující výšky ve 2D
terénu. Plošinou v tomto seznamu nazveme maximální souvislý úsek
stejné výšky délky alespoň 2.
Čistá funkce flats dostane na vstupu takový seznam a vrátí
seznam, v němž je každá plošina reprezentovaná její výškou, a to
ve stejném pořadí, v jakém jsou plošiny v původním seznamu.
Pojmem „náhorní plošina“ označíme v seznamu celých čísel souvislou
podposloupnost alespoň dvou stejných prvků, která ani z jedné
strany nesousedí s vyšším prvkem.
Čistá funkce rightmost_plateau dostane na vstup neprázdný seznam
celých čísel a pokud tento seznam obsahuje alespoň jednu náhorní
plošinu, tak vrátí index prvního prvku nejpravější náhorní plošiny
v seznamu; v opačném případě vrátí číslo -1.
def rightmost_plateau(heights):
pass
Příklad: Volání rightmost_plateau([2, 2, 4, 5, 5, 2]) vrátí 3, protože
seznam obsahuje jednu náhorní plošinu tvořenou čísly 5, první prvek této
plošiny je na indexu 3.
Volání rightmost_plateau([3, 3, 2, 4, 4]) vrátí 3, protože zadaný seznam
obsahuje dvě náhorní plošiny, první prvek té nejpravější je na indexu 3.
Volání rightmost_plateau([2, 2, 3, 3, 4]) vrátí -1, protože zadaný seznam
neobsahuje žádnou náhorní plošinu.
Čistá funkce largest_exponent dostane na vstup neprázdný seznam
kladných čísel numbers a prvočíslo prime a vrátí to ze
zadaných čísel, které má v prvočíselném rozkladu největší mocninu
zadaného prvočísla (pokud se tam zadané prvočíslo nevyskytuje, má
mocninu 0). Pokud je v seznamu více čísel se stejnou mocninou
zadaného prvočísla v rozkladu, vrátí to nejmenší z nich.
Tento týden se zaměříme na korektnost (správnost) programů –
zejména nás budou zajímat nástroje, které nám pomohou psát programy
bez chyb. K dispozici máme dvě základní kategorie takových nástrojů:
statické, totiž takové, které analyzují program aniž by jej
spouštěli – pracují podobně jako například edulint, který již
znáte,
dynamické, které kontrolují, zda program pracuje správně během
samotného provádění programu.
Tyto dva přístupy ke kontrole správnosti programu reprezentují
určitým způsobem opačné kompromisy. Dynamické nástroje jsou velice
přesné (umožňují kontrolovat prakticky libovolné, i velmi složité,
vlastnosti), ale nemůžou nám zaručit, že program se bude za všech
okolností chovat správně. Taková kontrola je často velmi časově
náročná, protože abychom si ověřili správnost programu, musíme jej
testovat: opakovaně spouštět s různými vstupy.
Statická kontrola je naopak méně přesná (umožňuje nám kontrolovat
pouze jednoduché vlastnosti programu), ale je rychlá (program není
potřeba spouštět) a může být bezpečná (tzn. některé statické
kontroly můžou zaručit, že určitý typ chyby v programu nikdy
za běhu nenastane).
V kategorii statických nástrojů jsou pro nás zajímavé zejména
typové anotace, které lze kontrolovat programem mypy. V tomto
předmětu máme již zkušenost s dynamickou typovou kontrolou, kdy
pokus například o sečtení čísla a řetězce vede na běhovou chybu,
tzn. program v momentě, kdy se takovou operaci pokusí provést,
havaruje s výjimkou TypeError. Typové anotace a statická typová
kontrola nám umožní většině podobných chyb předejít, aniž bychom
museli program spustit (natož důkladně testovat).
Z těch dynamických jsou pro nás přístupná zejména dynamická
tvrzení, která zapisujeme již známým klíčovým slovem assert.
Dynamická tvrzení nám zejména umožňují formalizovat a automaticky
při každém volání kontrolovat vstupní a výstupní podmínky funkcí
(podprogramů). Můžeme je také použít k zápisu a ověření dalších
podmínek, o kterých jsme přesvědčeni, že musí v daném místě programu
za každých okolností platit.
V obou případech (typové anotace a dynamická tvrzení) musíme do
programu přidat dodatečné informace, které netvoří přímo součást
výpočetní části programu (tzn. nepopisují samotné kroky výpočtu).
Mohlo by se na první pohled zdát, že přidávat tyto „přebytečné“
prvky do programu je práce navíc, která nás bude při programování
leda zdržovat. Trochu hlubší analýza ale odhalí, že počáteční zápis
programu tvoří jen zlomek celkového času, který programováním
strávíme – ladění a údržba typicky zaberou času mnohem víc.
Investice do anotací se většinou v těchto návazných fázích vývoje
programu velmi rychle vrátí.
Anotace plní 3 základní funkce:
nutí nás hlouběji se zamyslet o chování programu – často si
uvědomíme chybu už v čase, kdy uvažujeme jaké použít anotace,
umožňují použití automatických nástrojů pro kontrolu správnosti,
čím detekují chyby, které nám v prvním bodě přeci jen
proklouznou,
slouží jako dokumentace, jak pro programátory, kteří naše funkce
chtějí použít, tak pro pozdější úpravy a opravy v samotném kódu.
Tento týden si práci s anotacemi (zejména těmi typovými) nacvičíme
na příkladech. Nejprve ale jejich použití demonstrujeme v několika
ukázkách:
Hlavní novinkou této kapitoly jsou typové anotace. Ty se dotknou
zejména definice funkce a příkazu přiřazení. Rozšířený zápis
definice funkce má následovný tvar:
Význam všech anotací tvaru jméno: typ (tzn. jak v parametrech
funkcí, tak v přiřazení) je „jménovždy váže hodnotu typu
typ“. Význam anotace -> typ v definici funkce má pak význam
„návratová hodnota funkce je vždy typu typ“. Pravdivost těchto
tvrzení pak (staticky) ověří program mypy, jak již bylo naznačeno
v úvodě.
Na místě typ se ve výše uvedených formách může objevit:
jednoduchý typ:
bool – hodnota je True nebo False,
int – hodnota je celé číslo,
float – hodnota je číslo s plovoucí desetinnou čárkou,
str – hodnota je řetězec,
None – hodnota je None,
složený typ, který vznikne použitím typového konstruktoru
(tuple, list, atp.) a typových parametrů (píšeme
v hranatých závorkách za konstruktor; v těchto závorkách typ
představuje opět cokoliv z tohoto seznamu):
tuple[typ₁, typ₂, …, typₙ] – hodnota je -tice a její
-tá složka je typu typᵢ,
list[typ] – hodnota je seznam, kterého každý prvek je
typu typ,
tzv. volitelný typ, který vznikne zápisem typ | None, popisuje
hodnotu, která může být typu typ, nebo může být None (ale nic
jiného)8,
nebo tzv. typový alias, tedy jméno, které je přiřazením
svázáno s konkrétním typem (jména typových aliasů začínají velkým
písmenem):
Zápis pomocí „svislítka“ | umožňuje i obecnější typy, v tuto chvíli se ale omezíme na tvar typ | None. Komplikovanější typy tohoto tvaru zavedeme v sedmé kapitole.
V této části najdete popis některých častých typových chyb. Budeme
ji postupně doplňovat, pokud vám nějaká typová chyba není jasná,
můžete se zeptat v diskusním fóru. Nevkládejte tam však skutečný kód
ze svých řešení domácích úkolů. Pokuste se problém s anotacemi
izolovat do nějaké malé ukázky.
def longer_than_average_indices(data: list[str]) -> list[int]:
total_length = 0
for i in data:
total_length += len(i)
avg = total_length / len(data)
out = []
for i in range(len(data)):
if len(data[i]) > avg:
out.append(i)
return out
Pro tento kód dostaneme následující výstup z mypy:
longer.py:11: error: Incompatible types in assignment
(expression has type "int", variable has type "str")
longer.py:12: error: No overload variant of "__getitem__" of
"list" matches argument type "str"
longer.py:12: note: Possible overload variants:
longer.py:12: note: def __getitem__(self, int) -> str
longer.py:12: note: def __getitem__(self, slice) -> List[str]
longer.py:14: error: Incompatible return value type
(got "List[str]", expected "List[int]")
Found 3 errors in 1 file (checked 1 source file)
Obecně platí, že chyby je vhodné opravovat od začátku, protože další
chyby mohou být způsobeny těmi předchozími a samy o sobě tak nemusí
vždy dávat dobrý smysl.
První chyba se nachází na řádku s druhým for cyklem. Snažíme se
tu přiřadit do proměnné typu str výraz typu int. V tomto
případě se jedná o přiřazení do řídící proměnné cyklu a problém
je způsoben tím, že jsme použili jméno proměnné, kterou jsme
použili již v prvním cyklu, ale v tomto případě se ji snažíme
použít pro iteraci přes položky jiného typu.
Chyba je mimo jiné důsledkem toho, že řídící proměnné cyklů (a
obecně proměnné definované uvnitř cyklů) jsou v Pythonu
(na rozdíl od mnohých dalších jazyků) dostupné i po skončení
cyklu.
Chyby se zbavíme typicky tak, že použijeme jinou proměnnou.
Druhá chyba, ta na následujícím řádku, nám říká, že proměnná,
kterou se snažíme indexovat je špatného typu.
Tato chyba je v tomto případě důsledkem té první, ale může
samozřejmě nastat i samostatně. Mypy má již zapamatované, že i
je typu str a tedy předpokládá, že se pokoušíme indexovat
seznam řetězcem.
Poněkud neintuitivní je, že se v chybě neobjevuje indexace
pomocí hranatých závorek, ale metoda __getitem__. To je dáno
tím, že touto metodou je vnitřně indexace implementována.
Dva řádky „note“ říkají, že máme dvě možnosti, čím indexovat –
buď pomocí int nebo slice. Typ slice v IB111 nepoužíváme,
jako jediná možnost tedy zbývá indexování typem int.
Poslední chyba nám říká, že se snažíme vrátit hodnotu jiného
typu, než jaká byla očekávána (díky anotaci funkce).
I tato chyba je v tomto případě následkem té první.
Operátor ** je specifický v tom, že v závislosti na svých
argumentech může vracet různé typy, což komplikuje jeho použití
v otypovaném kódu. Uvažme následující funkci, která počítá nezápornou
mocninu čísla 2.
def power2(num: int) -> int:
assert num >= 0
return 2 ** num
Pro tento kód dostaneme následující výstup z mypy --strict:
pow.py:2: error: Returning Any from function declared to return "int"
Found 1 error in 1 file (checked 1 source file)
Problém je v tom, že výraz 2 ** num pro celočíselné num vrací buď int
(pokud je num ≥ 0) nebo float (pokud je num < 0). Řešení této situace
je dvojí:
pokud jste si jisti, že funkce power2 vždycky dostane jen nezáporné
parametry (jako v našem příkladě, kde je to vstupní podmínka funkce),
pak je řešením výraz nejprve přiřadit do anotované proměnné, tedy např.
nejprve provedeme result: int = 2 ** num a následně return result;
pokud funkce power2 může dostat i záporný parametr (tedy pokud bychom
rozvolnili výše uvedenou vstupní podmínku), pak je nejlepší vynutit, aby
výsledek byl vždy typu float, např. pomocí změny typu jednoho z operandů:
2.0 ** float; samozřejmě je pak třeba rovněž změnit typovou anotaci
návratové hodnoty funkce na -> float.
Typ Any se pak v chybové hlášce objevuje proto, že operátor **
je v Pythonu otypovaný tak, že vrací Any. Lze si představit i jiná
možná řešení, ale autoři mypy (resp. autoři typeshed, což je
projekt, který se zabývá typovými anotacemi pro standardní knihovny
a vestavěné funkce a operátory Pythonu) se (z dobrých důvodů)
rozhodli, že tomuto výrazu raději žádný typ nepřidělí.
V tomto příkladu budeme počítat základní vlastnosti geometrických
objektů, které budeme popisovat n-ticemi (zejména čísel). Příklad
slouží k seznámení s typovou anotací parametrů a návratových
hodnot podprogramů (funkcí).
Jak již víte z přednášky, anotace základních typů (int, float,
str, atp.) se zapisuje přímo názvem typu, zatímco anotace
složených typů mají trochu složitější zápis: seznamy zapisujeme
jako list[element] (kde element je typová anotace platná pro
každý prvek seznamu) a n-tice (zapisujeme jako tuple[x, y, z] –
tento zápis značí trojici, kde x, y a z jsou postupně typové
anotace pro první, druhou a třetí složku n-tice). Konečně případy,
kdy potřebujeme otypovat hodnotu, která je typu type, ale nemusí
nutně existovat (může být v některých případech None), použijeme
anotaci type | None.
Jako první si definujeme čistou funkci pro výpočet obsahu kruhu
(anglicky disc), která má jediný parametr typu float a jejíž
výsledkem je opět číslo typu float. Tím, že tyto skutečnosti
zapíšeme do programu jako anotace de-facto deklarujeme vstupní a
výstupní podmínky funkce: vstupní podmínkou je, že skutečná
hodnota předávaného parametru je typu float, zatímco výstupní
je, že návratová hodnota je též typu float. Pro jistotu
připomínáme, že za splnění vstupní podmínky zodpovídá
volající, zatímco za splnění výstupní podmínky zodpovídá
volaná funkce.
Program mypy nám pro takto anotovanou funkci zaručí dvě věci:
jednak, že omylem funkci nezavoláme se špatným typem parametru
(neporušíme vstupní podmínku na typy), třeba s hodnotou typu
řetězec. Dále pak kontroluje, že v těle funkce neporušujeme
výstupní podmínku – návratová hodnota je číslo typu float
(nevrátíme omylem v žádném příkazu return ve funkci třeba
řetězec, nebo None). K provedení této kontroly není potřeba
program spouštět.
Zatímco pro popis kruhu nám stačí jediné číslo, pro popis
obdélníku již potřebujeme čísla dvě, výšku a šířku. Máme dvě
možnosti: můžeme potřebné hodnoty předat jako dva samostatné
parametry, nebo můžeme obě hodnoty zabalit do n-tice (dvojice).
Druhý přístup je lepší v případě, kdybychom potřeboval vytvořit
třeba seznam obdélníků (to bude i náš případ). Proto zvolíme
přístup s dvojicí čísel. Někdy má smysl složitější typy
pojmenovat, a protože s obdélníky budeme pracovat na více
místech, zavedeme si pro typ dvojice čísel jméno Rectangle:
Rectangle = tuple[float, float]
Nyní již můžeme přistoupit k samotné definici (opět čisté) funkce
pro výpočet plochy obdélníku. Výsledkem bude opět číslo.
Elipsa reprezentuje podobný případ, kdy potřebujeme k jejímu
popisu dvě čísla, tentokrát délky jejích dvou poloos. Všimněte si,
že typ popisující elipsu je identický s typem pro obdélník. S tím
jsou spojeny určité problémy, které si objasníme níže. Protože
elipsami se nebudeme dále zabývat, nebudeme tentokrát typ
pojmenovávat.
def ellipse_area(semiaxes: tuple[float, float]) -> float:
major, minor = semiaxes
return pi * major * minor
Abychom demonstrovali i nehomogenní n-tice (tj. takové, které mají
složky různých typů), zadefinujeme si ještě pravidelný n-úhelník,
který zadáme hlavním poloměrem (tzn. vzdáleností vrcholu od
středu) a počtem vrcholů (který je na rozdíl od poloměru
celočíselný).
Nyní si definujeme funkci, která budou pracovat s trochu
složitějšími typy: vstupem bude seznam barevných obdélníků a jedna
vybraná barva, výsledkem bude celková plocha dané barvy. Pro barvu
(reprezentovanou řetězcem) si zavedeme typové synonymum: to je
typicky vhodné v případech, kdy se příslušný typ objevuje jako
složka n-tice. Uvažte rozdíl mezi čitelností typové anotace
tuple[tuple[int, int], str] vs. tuple[Rectangle, Colour].
Na tomto místě musíme mypy trochu pomoct, protože literál
0 lze interpretovat jako celé i jako desetinné číslo,
přičemž výchozí interpretace je celočíselná. V podstatě máme
dvě možnosti: můžeme literál zapsat jako 0.0, čím
nejednoznačnost odstraníme, nebo přidáme typovou anotaci i
proměnné (střadači) area. Taková anotace se zapisuje na
levou stranu přiřazení a syntakticky je stejná jako anotace
parametru.
area: float = 0
Cyklus pro sečtení ploch se už od zápisu, na který jsme
zvyklí, nijak neliší. Stojí nicméně za zmínku, že mypy za
nás kontroluje krom správného volání funkce rectangle_area
také to, že srovnáváme hodnoty stejných (obecněji
kompatibilních) typů – kdybychom omylem srovnali třeba řetězec
(barvu) a obdélník (třeba proto, že jsme zaměnili pořadí
rect a colour při rozbalování hodnoty typu
tuple[Rectangle, Colour]), mypy by nás na tuto chybu
upozornilo.
for rect, colour in rectangles:
if colour == selected_colour:
area += rectangle_area(rect)
return area
Dále napíšeme funkci, která ze seznamu obdélníků vybere ten
s největší plochou, existuje-li takový právě jeden. Je zde vidět,
že návratový typ může být, podobně jako typy parametrů, složitější
– připomínáme, že type | None znamená, že hodnota může být
buď typu type nebo None (vzpomeňte si také, že Rectangle je
synonymum pro tuple[float, float]).
for r in rectangles:
if isclose(rectangle_area(r), rectangle_area(largest)):
count += 1
elif rectangle_area(r) > rectangle_area(largest):
count = 1
largest = r
return largest if count == 1 else None
Konečně napíšeme funkci, která ze seznamu obdélníků vybere ty,
které mají plochu stejnou nebo větší, než je průměrná plocha
celého vstupního seznamu (který musí být neprázdný).
def large_rectangles(rectangles: list[Rectangle]) \
-> list[Rectangle]:
total = sum([rectangle_area(r) for r in rectangles])
average = float(total) / len(rectangles)
result = []
for r in rectangles:
if rectangle_area(r) >= average:
result.append(r)
return result
Na začátku jsme zmiňovali, že elipsu a obdélník reprezentujeme
stejným typem, a že by to mohlo vést k určitým problémům.
Samozřejmě, nemůže se stát nic horšího, než co by se stalo,
kdybychom anotace nepoužili vůbec, nicméně musíme si zároveň
uvědomit, že typové anotace nejsou všemožné, a ani před něčím,
co napohled vypadá jako typová chyba, nás nemusí ochránit.
Uvažte následující (zakomentovaný) příkaz – protože
unit_rectangle je typu tuple[float, float] a funkce
ellipse_area očekává parametr téhož typu, je z pohledu
mypy takové volání v pořádku. Přesto je zřejmé, že takovéto
použití nebylo zamýšleno, a téměř s jistotou povede k chybě
v programu. Tuto konkrétní situaci lze lépe řešit použitím
složených datových typů, které si ukážeme přespříští týden.
Tato ukázka je první z dvojice, která demonstruje použití
tvrzení (assertion) pro popis vstupních a výstupních podmínek.
Nejprve si v rychlosti zopakujme trochu teorie.
Velmi důležitá vlastnost tvrzení je, že ve správném (korektním)
programu musí za všech okolností platit. Dojde-li k porušení
některého tvrzení, program havaruje s chybou AssertionError a
vždy se jedná o chybu v programu. Je-li tedy uživatel schopen
programu předložit vstup, který způsobí, že program havaruje
s chybou AssertionError, tento program je špatně.
Smyslem takovýchto tvrzení tedy není kontrola vstupu, nebo jiných
okolností, které můžou selhat – naopak, slouží jako dokumentace a
pomůcka k ladění: odhalit příčinu chybného chování programu je tím
snazší, čím dříve si všimneme nějakou odchylku od chování
očekávaného. Budeme-li důsledně kontrolovat vstupní a výstupní
podmínky příkazy assert, je pravděpodobné, že chybu odchytíme
brzo (program havaruje).
Naopak, budeme-li spoléhat na vlastní neomylnost (případně
neomylnost kolegů), ale chyba se do programu přeci dostane, bude
se pravděpodobně nekontrolovaně šířit – funkce, kterých vstupní
podmínka nebyla splněna jednoduše vypočtou nesprávný výsledek, se
kterým bude program nadále pracovat a produkovat další a další
nesmyslné mezivýsledky. Výstup nebo chování programu bude
nesprávné, ale bude velice obtížné a časově náročné poznat, ve
kterém kroku výpočtu došlo k první chybě.
Nyní již můžeme přejít k ukázkovému programu: téma první části
budou čárové kódy. V tomto modulu se budeme zabývat samotným
kódováním sekvence černých a bílých pruhů, zatímco v části druhé
(ean.py) se budeme zabývat již dekódovanými číselnými hodnotami.
Čárový kód sestává z řady pruhů (anglicky area), kde každý pruh
může být černý nebo bílý. Pruhy zabírají celou výšku kódu a mají
fixní šířku, přičemž na šířku se vždy dotýkají: dva sousední černé
pruhy tvoří jednolitou plochu. Každá číslice je kódována do sedmi
pruhů, třeba číslice 2 vypadá takto (v binárním zápisu 0010011; na
obrázku je šířka jednoho pruhu přehnaná, skutečné pruhy jsou velmi
úzké).
Každá číslice má 3 různá možná kódování, značená L, R a G,
přičemž v kódech EAN-8, se kterými budeme pracovat, se objevují
pouze kódování L a R, která jsou vzájemně inverzní: obrázek
výše je v kódování L, odpovídající kódování R je následovné:
Čárové kódy standardu EAN mají 5 skupin pruhů:
počáteční skupina, vždy 101,
první polovina číslic (každá kódována do sedmi pruhů),
středová dělící skupina, vždy 01010,
druhá polovina číslic (opět po sedmi pruzích),
koncová skupina, vždy 101.
Následuje kompletní příklad se dvěma číslicemi (2 a 2), první
kódovanou v L a druhou v R. Pro odlišení jsou pruhy koncových
a středové skupiny vybarveny světlejší barvou a místo 0 a 1
používají symboly _ a X:
def digit_count(num: int, base: int) -> int:
result = 0
while num > 0:
num //= base
result += 1
return result
def digit_slice(num: int, base: int, low: int, size: int) -> int:
return num // base ** low % base ** size
Jako první definujeme predikát barcode_valid, který bude
kontrolovat platnost kódu (tzn. má-li požadovanou strukturu a
správně zakódované číslice). Protože se jedná o relativně složitý
predikát, některé kontroly oddělíme do samostatných funkcí (mnoho
z nich navíc později využijeme při dekódování). Krom samotného
čárového kódu má funkce parametry digit_count (počet očekávaných
číslic kódu), l_coding je požadované kódování levé číselné části
(L nebo R) a r_coding pravé číselné části.
Nejprve zkontrolujeme, má-li čárový kód správnou délku: musí
obsahovat dvě krajové a jednu středovou skupinu a sudý počet
pruhů, které kódují číslice.
if bit_count(barcode) < total_marker_size:
return False # not enough space for all required markers
if (bit_count(barcode) - total_marker_size) % 2 != 0:
return False # does not evenly split into halves
Dále prověříme, že krajové a středová značka mají správné
hodnoty.
if bit_slice(barcode, 0, boundary_size) != 0b101:
return False # bad start marker
if bit_slice(barcode, center_end + half_width, 3) != 0b101:
return False # bad end marker
if bit_slice(barcode, center_start, center_size) != 0b01010:
return False
Nakonec zkontrolujeme, že má správně zakódované číslice. Zde
uplatníme několik pomocných funkcí, kterých definice uvidíme
později: (čistá) funkce barcode_digits z čárového kódu
extrahuje dvě číslice-kódující oblasti, predikát
barcode_valid_digits ověří, že vstupní číselná oblast
správně kóduje číslice.
if half_width % 7 != 0:
return False
if 2 * half_width // 7 != digit_count:
return False
left, right = barcode_digits(barcode)
if not barcode_valid_digits(left, l_coding):
return False
if not barcode_valid_digits(right, r_coding):
return False
return True
Pomocná funkce pro výpočet délky jedné ze dvou číslicových oblastí
čárového kódu, v počtu pruhů. Vstupní podmínkou je správná délka
kódu (taková, aby se dal správně rozdělit na příslušné oblasti).
Vstupní podmínku opět zapíšeme pomocí příkazů assert.
Jak již bylo zmíněno, funkce barcode_digits extrahuje z čárového
kódu dvě číselné oblasti. Potřebné vstupní podmínky již kontroluje
pomocná funkce barcode_half_width kterou hned na začátku voláme,
nebudeme je tedy ve funkci barcode_digits opakovat.
Dále potřebujeme být schopni kódovat a dekódovat jednotlivé
číslice, k čemu nám poslouží následující dvojice funkcí. V druhém
parametru zadáváme, které kódování číslic požadujeme (L nebo
R). V kódovací funkci je vstupní podmínkou jednak správnost
druhého parametru, ale také to, že digit je skutečně jediná
číslice.
for _ in range(7):
area = bits % 2
bits //= 2
if coding == 'L':
code += area * shift
if coding == 'R':
code += (1 - area) * shift
shift *= 2
return code
Dekódování číslic provedeme „hrubou silou“ (lze to i lépe, ale pro
tuto chvíli k tomu úplně nemáme ty správné jazykové prostředky).
Vstupní podmínkou je, že code je nezáporné číslo. Nepovede-li
se číslici v zadaném kódování přečíst, funkce vrátí None.
def barcode_decode_digit(code: int, coding: str) -> int | None:
assert code >= 0
for digit in range(10):
if barcode_encode_digit(digit, coding) == code:
return digit
return None
Nyní jsme již připraveni definovat predikát, který bude
kontrolovat správné kódování dané číselné oblasti. Jednak musí
ověřit správnou délku. Jestli délka vyhovuje, opakovaným použitím
funkce barcode_decode_digit se pokusíme jednotlivé číslice
přečíst – selže-li tato funkce na některé skupině sedmi pruhů, je
kód neplatný.
def barcode_valid_digits(areas: int, coding: str) -> bool:
base = 2 ** 7
while areas > 0:
if barcode_decode_digit(areas % base, coding) is None:
return False
areas //= base
return True
Konečně můžeme přistoupit k samotnému kódování a dekódování
číselných oblastí čárového kódu. Dekódovat lze pouze platnou
číselnou oblast, vstupní podmínkou je tedy pravdivost predikátu
barcode_valid_digits. Je tedy odpovědnost volajícího špatné
čárové kódy zamítnout před pokusem o jejich dekódování (lze k tomu
využít třeba právě predikátu barcode_valid_digits, není-li
platnost zaručena jinak).
while areas > 0:
digit = barcode_decode_digit(areas % base, coding)
areas //= base
Protože v areas je uložena platná číselná oblast, musí
se nám povést každou jednotlivou číslici dekódovat.
assert digit is not None
result += digit * shift
shift *= 10
return result
Zbývá poslední funkce, která ze zadaných číslic vytvoří číselnou
oblast čárového kódu. Vstupní podmínkou je zde pouze to, že
vstupní číslo je nezáporné.
Výstupní podmínkou je, že jsme vytvořili platnou číselnou oblast.
Vzpomeňte si, že výstupní podmínka je (v případě čisté funkce)
vlastnost návratové hodnoty, kterou funkce sama zaručuje. Výstupní
podmínku zapisujeme jako tvrzení (assert) před návratem
z funkce.
def barcode_encode(digits: int, coding: str) -> int:
assert digits >= 0
result = 0
base = 2 ** 7
shift = 1
while digits > 0:
result += barcode_encode_digit(digits % 10, coding) * shift
shift *= base
digits //= 10
assert barcode_valid_digits(result, coding)
return result
European Article Number (EAN) je systém číslování výrobků, který
pravděpodobně znáte z čárových kódů v supermarketech. EAN funguje
podobně jako ISBN, se kterým jste minulý týden pracovali
v příkladu 04/isbn.py, nicméně neomezuje se na knihy. V této
ukázce budeme pokračovat v používání tvrzení (assert) pro
popis vstupních a výstupních podmínek funkcí. Protože budeme chtít
převádět číselné kódy na čárové a obráceně, využijeme funkce pro
práci s čárovými kódy, které jsme definovali v předchozí ukázce.
Podobně jako v případě ISBN budeme EAN reprezentovat jako číslo.
Jako první si zadefinujeme predikát, který bude rozhodovat,
jedná-li se o platný EAN: postup je podobný jako pro ISBN,
poslední cifra je kontrolní. EAN existuje v několika délkách, ale
algoritmus pro jejich kontrolu je vždy stejný: proto dostane náš
predikát krom samotného EAN jako parametr i očekávanou délku kódu.
Tento predikát samotný nemá žádné vstupní podmínky.
Další funkce, kterou budeme definovat, slouží k vytvoření platného
EAN-13 kódu z jednotlivých komponent: prefixu GS1 (zjednodušeně
odpovídá zemi výrobce), kódu výrobce (který je minimálně
pěticiferný) a kódu samotného výrobku. Vstupní podmínky odpovídají
omezením na jednotlivé komponenty. Celková délka kódu bez
kontrolního součtu musí být 12 cifer. Funkce komponenty zkombinuje
a přidá kontrolní cifru. Výstupní podmínkou je, že jsme vytvořili
platný třináctimístný EAN kód (kontrolujeme ji těsně před návratem
z funkce).
Následují dvě funkce pro konverzi mezi číselným a čárovým kódem.
První dostane na vstupu platnou číselnou reprezentaci EAN-8 (tuto
vstupní podmínku kontroluje první příkaz assert). Výstupní
podmínkou naopak je, že funkce vytvoří platný čárový kód – tuto
kontrolujeme, jak je obvyklé, těsně před návratem.
def ean8_to_barcode(ean: int) -> int:
assert ean_valid(ean, 8)
left = barcode_encode(decimal_slice(ean, 4, 4), 'L')
right = barcode_encode(decimal_slice(ean, 0, 4), 'R')
Poslední funkce v tomto souboru slouží pro opačnou konverzi:
z čárového kódu vytvoří číselnou reprezentaci. Vstupní podmínkou
je, že čárový kód je platný a kóduje 8 číslic; toto díky predikátu
barcode_valid lehce ověříme. Nicméně si musíme dát pozor na
výstupní podmínku: mohlo by se zdát, že analogicky k předchozímu
případu by bylo rozumné požadovat platnost číselného EAN.
Není tomu tak: byla-li splněna vstupní podmínka (čárový kód
barcode je platný), funkce musí svoji výstupní podmínku vždy
splnit. Musíme si ale uvědomit, že existují platné osmičíslicové
čárové kódy, které nekódují platný EAN-8. Proto je výstupní
podmínka platnosti EAN kódu příliš silná – nedokážeme ji
zabezpečit.
Jako vhodné řešení se jeví v případě, kdy na vstupu dostaneme
čárový kód reprezentující neplatný EAN, vrátit hodnotu None:
výstupní podmínku tak zeslabíme jen minimálně. Bude vždy platit,
že výstupem je buď platný EAN-8 (a to vždy, když je to možné),
nebo hodnota None (pouze v případech, kdy vstup reprezentoval
neplatný EAN-8). Ze zápisu návratové hodnoty je zřejmé, že tato
výstupní podmínka je splněna, nemá tedy smysl ji dodatečně
kontrolovat příkazem assert.
def barcode_to_ean8(barcode: int) -> int | None:
assert barcode_valid(barcode, 8, 'L', 'R')
left, right = barcode_digits(barcode)
ean = decimal_compose(barcode_decode(left, 'L'),
barcode_decode(right, 'R'), 4)
if not ean_valid(ean, 8):
return None
return ean
Otypujte následující funkce tak, aby prošla typová kontrola
s přiloženými testy.
Funkce degrees konvertuje radiány na stupně.
def degrees(radians):
return (radians * 180) / pi
Funkce to_list rozdělí číslo na číslice o daném základu.
def to_list(num, base):
digits = []
result = []
while num > 0:
digits.append(num % base)
num //= base
for i in range(len(digits)):
result.append(digits[-i - 1])
return result
Funkce diagonal vytvoří seznam obsahující prvky na diagonále
matice matrix.
def diagonal(matrix):
diag = []
for i in range(len(matrix)):
diag.append(matrix[i][i])
return diag
Funkci with_id je v parametru elements předán seznam dvojic
(celočíselný klíč, řetězec). Funkce najde prvek s klíčem id_ a
vrátí odpovídající řetězec.
def with_id(elements, id_):
for element_id, val in elements:
if id_ == element_id:
return val
return None
Funkce update_students v seznamu studentů, zadaných trojicemi
(učo, jméno a volitelně rok ukončení studia) všem studentům, kteří
ještě nemají studium ukončené, nastaví rok ukončení studia na
zadaný.
def update_students(students, end):
result = []
for uco, name, graduated in students:
if graduated is None:
graduated = end
result.append((uco, name, graduated))
return result
Predikát is_increasing je pravdivý, pokud je seznam celých čísel
seq rostoucí.
def is_increasing(seq):
for i in range(1, len(seq)):
if seq[i - 1] >= seq[i]:
return False
return True
def fridays(year, day_of_week):
count = 0
for month in range(1, 13):
days = days_per_month(year, month)
for day in range(1, days + 1):
if is_friday(day_of_week) and day == 13:
count += 1
day_of_week = (day_of_week + 1) % 7
return count
V této úloze budete pracovat s databázovou tabulkou. Tabulka je
dvojice složená z hlavičky a seznamu záznamů. Hlavička
obsahuje seznam názvů sloupců. Jeden záznam je tvořen seznamem
hodnot pro jednotlivé sloupce tabulky (pro jednoduchost uvažujeme
jenom hodnoty typu řetězec). Ne všechny hodnoty v záznamech musí
být vyplněny – v tom případě mají hodnotu None.
Vaším úkolem bude nyní otypovat a implementovat následující
funkce. Funkce get_header vrátí hlavičku tabulky table.
def get_header(table):
pass
Funkce get_records vrátí seznam záznamů z tabulky table.
def get_records(table):
pass
Procedura add_record přidá záznam record na konec tabulky
table. Můžete předpokládat, že záznam record bude mít stejný
počet sloupců jako tabulka.
def add_record(record, table):
pass
Predikát is_complete je pravdivý, neobsahuje-li tabulka table
žádnou hodnotu None.
def is_complete(table):
pass
Funkce index_of_column vrátí index sloupce se jménem name.
Můžete předpokládat, že sloupec s jménem name se v tabulce
nachází. První sloupec má index 0.
def index_of_column(name, header):
pass
Funkce values vrátí seznam platných hodnot (tzn. takových, které
nejsou None) v sloupci se jménem name. Můžete předpokládat, že
sloupec se jménem name se v tabulce nachází.
def values(name, table):
pass
Procedura drop_column smaže sloupec se jménem name z tabulky
table. Můžete předpokládat, že sloupec se jménem name se
v tabulce nachází.
def drop_column(name, table):
pass
Konečně otypujte následující dvě testovací funkce (jejich
implementaci neměňte, pouze přidejte typové anotace).
Vraťme se k ukázkovému příkladu 03/points.py, kde Vám byly
představeny n-tice. Při takto komplikovaných typech je vhodné
funkce otypovat, jak pro čitelnost, tak pro jednodušší hledání
chyb. Vaším úkolem bude nyní otypovat funkce i testovací procedury
a případně proměnné, tak, aby Vám prošla typová kontrola.
Doporučujeme si zavést typové aliasy pro opakující se jednoznačně
pojmenovatelné typy.
Funkce distance spočte Euklidovskou vzdálenost dvou bodů a a b.
Funkce leftmost_colour v neprázdném seznamu bodů najde barvu
„nejlevějšího“ bodu (takového, který má nejmenší x-ovou souřadnici).
def leftmost_colour(points):
x_min, _, result = points[0]
for x, _, colour in points:
if x < x_min:
x_min = x
result = colour
return result
Dále funkce center_of_gravity dostane jako parametry seznam bodů
points a barvu colour; jejím výsledkem bude bod, který se
nachází v těžišti soustavy bodů dané barvy (a který bude stejné
barvy). Vstupní podmínkou je, že points obsahuje alespoň jeden
bod barvy colour.
Jako poslední si definujeme funkci average_nonmatching_distance,
která spočítá průměrnou vzdálenost bodů různé barvy. Vstupní
podmínkou je, že seznam points musí obsahovat alespoň dva
různobarevné body.
def average_nonmatching_distance(points):
total = 0.0
pairs = 0
for i in range(len(points)):
for j in range(i):
_, _, i_colour = points[i]
_, _, j_colour = points[j]
if i_colour != j_colour:
total += distance(points[i], points[j])
pairs += 1
V této úloze bude Vaším úkolem implementovat a otypovat
následující funkce, které implementují dotazy na školní kurzy.
Kurz je reprezentován seznamem dvojic (student, známka), přičemž
student je trojice (učo, jméno, semestr) a známka je řetězec
z rozsahu A až F.
Funkce failed vrátí seznam studentů kurzu course, kteří z něj
mají známku F.
def failed(course):
pass
Funkce count_passed vrátí počet studentů, kteří úspěšně ukončili
kurz course, tedy z něj nemají známku F. Parametr semester
je volitelný: je-li specifikován (není None), funkce vrátí počet
úspěšných studentů v daném semestru, jinak vrátí počet všech
úspěšných studentů.
def count_passed(course, semester):
pass
Funkce student_grade vrátí známku studenta s učem uco. Pokud
takový student v kurzu course není, vrací None.
V této úloze bude Vašim úkolem rozšířit a otypovat implementaci
z ukázky 02/triangle.py.
Strany trojúhelníku značíme . Úhel mezi a je
(gamma), mezi b a c je (alpha) a mezi a
je úhel (beta):
Prvním úkolem bude implementovat obecnou funkci perimeter, která má
volitelné parametry tří stran a tří úhlů trojúhelníku. Je-li to možné
z předaných parametrů, funkce spočítá obvod trojúhelníku jednou z metod
SSS, ASA, SAS, jinak vrátí None.
Druhým úkolem bude otypovat zbytek pomocných funkcí tak, aby Vám prošla
typová kontrola. Typ funkce perimeter neměňte.
V této úloze bude Vaším úkolem implementovat funkce pracující se
seznamem pacientů patients u lékaře. Každý pacient má záznam
(dvojici), který obsahuje jeho unikátní identifikátor a seznam
návštěv s výsledky. Návštěva je reprezentovaná čtveřicí – rokem,
kdy pacient navštívil lékaře, a naměřenými hodnotami: pulz,
systolický a diastolický tlak. Seznam návštěv pacienta je
uspořádaný vzestupně od nejstarší. Můžete předpokládat, že každý
pacient má alespoň jeden záznam.
Vaším prvním úkolem bude implementovat a otypovat funkci
missing_visits, která zjistí, kteří pacienti nebyli na prohlídce
od roku year. Jako výsledek vraťte seznam identifikátorů
pacientů.
def missing_visits(year, patients):
pass
Dále napište a otypujte funkci patient_reports, která vrátí
seznam zpráv o pacientech. Zpráva o pacientovi je čtveřice, která
obsahuje záznam o jeho nejvyšším doposud naměřeném pulzu a pro
každou měřenou hodnotu informaci, zda se měření dané hodnoty
v jednotlivých letech konzistentně zvyšují (True nebo False).
Například zpráva o pacientovi (1, [(2015, 91, 120, 80), (2018,
89, 125, 82), (2020, 93, 120, 88)]) je (93, False, False,
True).
Napište čistou funkci, která na vstupu dostane dvě celá kladná
čísla rows a cols a vrátí tabulku (dvourozměrný seznam)
o rows řádcích a cols sloupcích. V buňce v řádku y a sloupci
x bude počet společných dělitelů čísel x a y. Levý horní roh
má souřadnice x = y = 1.
Například pro vstup rows = 4, cols = 2 dostaneme tabulku [[1,
1], [1, 2], [1, 1], [1, 2]].
Opět je Vašim úkolem do již hotového programu doplnit typové
anotace tak, aby prošel kontrolou nástrojem mypy. Zároveň si zde
můžete procvičit porozumění kódu (budete-li vědět, co která funkce
dělá, typové anotace se Vám budou vymýšlet lépe).
def cell_value(grid, x, y):
if 0 <= x < len(grid) and 0 <= y < len(grid):
return grid[x][y]
return 0
def live_neighbour_count(grid, x, y):
assert x < len(grid) and y < len(grid)
res = 0
for row in range(x - 1, x + 2):
for col in range(y - 1, y + 2):
res += cell_value(grid, row, col)
return res - grid[x][y]
def next_value(grid, x, y):
assert x < len(grid) and y < len(grid)
Tento příklad bude mírně nekonvenční v tom, že nebudete
programovat nové funkce. Vaším úkolem bude naopak poznat, co
zadaná funkce počítá a napsat testy, které Vaši hypotézu ověří.
Každá funkce zde zadaná je predikát a většina má nějakou vstupní
podmínku. Samotné funkce i proměnné v nich jsou záměrně
pojmenované tak, aby Vám názvy nic neřekly.
def f_1(x: int, y: int) -> bool:
assert y >= 1
assert x >= 1
a = 0
b = 1
while x > 1:
c = a + b
a = b
b = c
x -= 1
return b == y
def test_f_1() -> None:
pass
def f_2(x: int, y: int) -> bool:
assert x > 0
b = 1
a = x // 2
while a >= b:
if x % b == 0:
y -= 1
b += 1
return y <= 1
def test_f_2() -> None:
pass
def f_3(x: int, y: int) -> bool:
assert x > 0 and y > 0
a = 1
b = 0
while a <= max(x, y):
if x % a == 0:
b += 1
if y % a == 0:
b -= 1
a += 1
return b > 0
def test_f_3():
pass
def f_4(x: int, y: int) -> bool:
for z in range(1, x):
b = True
for i in range(2, floor(sqrt(z)) + 1):
if z % i == 0:
b = False
if b:
y -= 1
return y == 0
def test_f_4():
pass
def f_5(x: int) -> bool:
assert x >= 0
y = 0
z = x
while z > 0:
y = y * 7 + z % 7
z = z // 7
return x == y
def test_f_5():
pass
def f_6(x: int, y: int) -> bool:
assert x >= 0
z = 0
while x > 0:
z = z * 2 + (x % 2)
x = x // 2
return y == z
def test_f_6() -> None:
pass
def f_7(x: int, y: int) -> bool:
assert x >= 0
z = 2
while x > 1:
if x % z == 0:
y -= 1
while x % z == 0:
x = x // z
z += 1
return y == 0
def test_f_7() -> None:
pass
def f_8(x: int, y: int, z: int) -> bool:
assert x > 0 and y > 0
d = 2
r = 0
while x > 1 and y > 1:
if x % d == 0 and y % d == 0:
x = x // d
y = y // d
r += 1
while x % d == 0:
x = x // d
while y % d == 0:
y = y // d
d += 1
return r == z
† V tomto příkladu se budeme zabývat polynomy, které
pravděpodobně znáte ze střední školy. Jestli ne, stačí Vám v tuto
chvíli vědět, že se jedná o výrazy tvaru
Hodnotám říkáme koeficienty. Koeficienty budeme reprezentovat
pomocí zlomků (zlomky proto, že je chceme dělit a násobit, aniž
bychom se dopouštěli nepřesnosti spojené s hodnotami typu
float). V Pythonu k tomu můžeme použít typ Fraction, který je
součástí standardní knihovny.
Polynom jako celek budeme reprezentovat jako seznam koeficientů:
na -tém indexu bude uložena hodnota . Z tohoto indexu je
také zřejmé, k jaké mocnině se koeficient váže (je to ).
Polynomial = list[Fraction]
Vaším úkolem bude implementovat 2 operace: derivaci (angl.
differentiation) a integraci. Derivací polynomu
je polynom kde koeficienty získáme ze
vztahu (pomyslný nulový koeficient do
seznamu ukládat nebudeme).
Integrace je opačná operace k derivaci: opět uvažujme , pak integrál bude mít koeficienty
, kde C je libovolná konstanta. Pro
jednoduchost budeme uvažovat .
Poslední úlohou je ověřit, že operace jsou skutečně vzájemně
inverzní. Napište funkci, která toto ověří. Protože derivace
„zapomíná“ hodnotu (při výpočtu nových koeficientů se vůbec
nepoužije), ověřit můžeme pouze jedno pořadí složení obou operací.
Rozmyslete si které to je. Opačný směr ověřte tak dobře, jak to
lze.
Přepište funkci tak, aby dosáhla stejného výstupu pouze pomocí
manipulace prvků ve stávajícím seznamu (tedy bez vytváření nového
seznamu)
Formulujte vstupní podmínku funkce mystery_function
# python
def mystery_function(nums):
result = [0] * len(nums)
i = 0
for num in nums:
if num % 2 == 0:
result[i] = num // 2
i += 1
for num in nums:
if num % 2 != 0:
result[i] = num * 2
i += 1
return result
Odhalte, co dělá následující funkce a zjednodušte ji.
Opět netradiční úloha: tentokrát budete doplňovat vstupní
podmínky, opět k funkcím, které jsou zapsané bez jakýchkoliv
užitečných názvů nebo komentářů. Vstupní podmínky doplňujte do
samostatných funkcí (predikátů) k tomuto účelu nachystaných.
Vstupní podmínka musí zaručit, že funkce skončí a splní výstupní
podmínku. Zároveň by měla co nejméně omezit použitelnost funkce
(tzn. měla by povolit co nejvíce vstupů).
def f_1(x_0: int, y: int) -> int:
assert precondition_1(x_0, y)
x = x_0
z = 0
s = -1 if (x < 0) != (y < 0) else 1
while abs(x) > 0:
x -= s * y
z += s
assert x_0 // y == z
return z
def f_2(x_0: int, y_0: int) -> int:
assert precondition_2(x_0, y_0)
x = x_0
y = y_0
z = 0
while x != y:
x += 1
y -= 1
z += 2
assert x_0 + z == y_0
return z
def f_3(x: int, y: int) -> int:
assert precondition_3(x, y)
i = 2
j = 1
while i <= min(x, -y):
if x % i == 0 and y % i == 0:
j = i
i += 1
assert j == gcd(x, y)
return j
def f_4(x_0: int, y: int) -> tuple[int, int]:
assert precondition_4(x_0, y)
x = x_0
z = 0
while x >= y:
x -= y
z += 1
assert z * y + x == x_0
assert z >= 0 and x >= 0
return (z, x)
Fibonácci používají k zápisu kladných celých čísel Fibonacciho soustavu.
Ta používá jen dvě číslice 0 a 1; řády čísel ovšem nejsou mocniny dvou
jako v klasické dvojkové soustavě, ale jsou postupně zprava 1, 2, 3, 5,
8, 13, … (Jde tedy o Fibonacciho čísla bez úvodních 0 a 1.)
Některá čísla je takto možno zapsat dvěma různými způsoby, např. číslo
se zapíše buď jako nebo jako .
Platí totiž .
Proto se zavádí tzv. kanonický zápis čísla ve Fibonacciho soustavě,
kdy se zakazuje mít vedle sebe dvě jedničky.
Čistá funkce fib_ones spočítá, kolik jedniček je v kanonickém
Fibonacciho zápisu nezáporného celého čísla num.
Příklady:
V kanonickém Fibonacciho zápisu čísla jsou tři jedničky, viz výše.
V kanonickém Fibonacciho zápisu čísla je jedna jednička (je to totiž
přímo Fibonacciho číslo).
V kanonickém Fibonacciho zápisu čísla jsou čtyři jedničky, protože
platí .
Magický čtverec je dvourozměrná matice vzájemně různých kladných celých
čísel, pro niž platí, že součty čísel v každém řádku, každém sloupci a
obou hlavních úhlopříčkách jsou stejné. Klasickým příkladem je magický
čtverec 3x3:
8 1 6
3 5 7
4 9 2
v němž se součty všech řádků, všech sloupců a obou diagonál rovnají 15.
Napište predikát is_magic_square, který na vstupu dostane dvourozměrné pole
celých čísel a ověří, že se jedná o magický čtverec.
Čistá funkce gambling_score ohodnotí výsledek hozený na kostkách
(neprázdný seznam celých čísel od 1 do 6 včetně) takto:
Trojice stejných čísel se boduje jako 100× hozené číslo, kromě trojice
jedniček, která je za 1000. Čtveřice stejných čísel se počítá za
dvojnásobek hodnoty trojice, pětice se počítá za dvojnásobek hodnoty
čtveřice atd. Pokud po započítání všech trojic, čtveřic, pětic atd. zbudou
nějaké (dosud nezapočítané) jedničky a pětky, počítá se každá jednička za
sto bodů, každá pětka za padesát bodů. Získané body se sečtou.
Příklad: Pro vstup [1, 1, 1, 1, 5, 3, 3, 3, 4] funkce vrátí 2350
(čtveřice jedniček za 2000 bodů, trojice trojek za 300 bodů, jedna pětka
za 50).
Pro vstup [2, 2, 5, 2, 2, 5, 2, 2] funkce vrátí 1700
(šestice dvojek za 1600 bodů, dvě pětky za 100).
Pro vstup [2, 2, 3, 4, 6, 6] funkce vrátí 0
(není zde žádná trojice ani lepší skupina stejných čísel, žádné jedničky,
žádné pětky).
Všimněte si zejména, že na pořadí čísel v seznamu nezáleží a že počítáme
vždy maximální množství výskytů daného čísla (tedy poté, co jsme v prvním
příkladu započítali čtveřici jedniček za 2000 bodů, už neuvažujeme o tom,
kolik trojic jedniček v seznamu je).
Napište čistou funkci nth_smallest_prime_divisor, která vrátí index-té
nejmenší prvočíslo vyskytující se v prvočíselném rozkladu čísla num.
Pokud se v rozkladu vyskytuje některé prvočíslo vícekrát, počítáme všechny
jeho výskyty, tedy např. v čísle je třetím
nejmenším prvočíslem číslo 3. Pokud má num méně než index prvočísel
v rozkladu, funkce vrátí None.
Předpokládejte, že num i index jsou kladná celá čísla.
Zde indexujeme od 1, tedy první prvočíslo v rozkladu má index 1.
Je potřeba, aby vaše funkce fungovala rozumně rychle i pro velmi velká
čísla, u nichž je hledané prvočíslo malé. (Není třeba vymýšlet zvláště
chytrá řešení, jen je třeba nedělat zbytečnou práci navíc.)
herní plán je jednorozměrný, s neomezenou délkou a vyznačeným startovním
políčkem;
každý hráč má jednu figurku, na začátku umístěnou na startovním políčku;
hráči střídavě hází kostkou a posunují své figurky o hozené číslo;
pokud by hráčova figurka měla vstoupit na políčko obsazené figurkou
jiného hráče, tato figurka je „vykopnuta“ (jako v Člověče, nezlob se)
zpět na start.
Situaci na herním plánu budeme reprezentovat pomocí nezáporného celého čísla
tak, že jeho zápis v pětkové soustavě reprezentuje obsazenost jednotlivých
políček bez startovního políčka. Číslice 0 reprezentuje prázdné políčko,
číslice 1–4 pak reprezentují obsazenost figurkou konkrétního hráče. Pohyb
figurek přitom v pětkovém zápisu probíhá „zprava doleva“, tedy směrem od
nižších řádů k vyšším.
Příklady:
Všechny figurky jsou na startu – stav reprezentovaný číslem 0.
Figurky hráčů 1 a 3 jsou na startu, figurka hráče 2 je dvě políčka od startu,
figurka hráče 4 je šest políček od startu. Tento stav je reprezentovaný
číslem .
Napište čistou funkci play, která na plánu reprezentovaném číslem arena
provede jeden tah hráče player o zadaný hod kostkou throw a vrátí
číslo reprezentující nový stav hry.
Předpokládejte, že arena je validní stav hry (tj. nezáporné celé číslo,
v jehož pětkovém zápisu se objevuje každá z číslic 1–4 nejvýše jednou),
že player je jedno z čísel 1, 2, 3, 4 a že throw je kladné celé číslo.
(Nemusí být nijak shora omezené; předpokládejte, že máme kostky s různě
velkými čísly.)
Mankala je souborné označení deskových her pro dva hráče, jejichž
společným znakem je přemisťování kuliček (kamínků, pecek, apod.) mezi důlky.
V tomto domácím úkolu si naprogramujete jednoduchou variantu takové hry –
pravidla jsou inspirována hrou Kalaha, resp. jednou z jejích obměn.
Hrací deska sestává z dvou řad menších důlků (jejich počet je parametrem
hry, viz níže) a dvou větších důlků vlevo a vpravo. Vypadá tedy např. takto
(počet menších důlků v každé řadě je zde šest):
Hru hrají dva hráči, kteří sedí proti sobě. Každému hráči patří menší
důlky na jeho straně a větší důlek vpravo – tento větší důlek nazýváme
hráčovou bankou. Na začátku hry je v každém menším důlku předem určený
počet kuliček (toto je druhý parametr hry), banky jsou prázdné. Hra probíhá
po kolech, přičemž se hráči střídají. Průběh každého kola je následující:
Hráč si vybere jeden ze svých menších důlků, který obsahuje nějaké
kuličky. Pokud jsou všechny důlky hráče prázdné, hra končí (viz níže).
Hráč vezme všechny kuličky z vybraného důlku a začne je po jedné
rozdělovat do následujících důlků proti směru hodinových ručiček, včetně
svého banku, ale ne do banku soupeře.
Pokud tedy např. spodní hráč vzal kuličky z důlku C, pak je bude postupně
rozdělovat do důlků D, E, F, G, H, I, J, K, L, M, A, B, C, atd., dokud
mu nějaké kuličky budou zbývat.
Pokud při rozdělování padla poslední kulička do prázdného menšího důlku
na straně aktuálního hráče a jeho oponent má v protějším důlku nějaké
kuličky, sebere hráč svou poslední kuličku a všechny kuličky
v protějším důlku a přesune je do své banky.
Pokud při rozdělování padla poslední kulička do hráčovy banky, v dalším
kole hraje tentýž hráč znovu; v opačném případě se hráči vystřídají.
Hra končí, když má hráč, který je na tahu, všechny menší důlky prázdné.
Jeho protivník si pak přesune všechny kuličky ze svých menších důlků do své
banky. Vyhrává ten hráč, který má v bance více kuliček.
Hrací desku reprezentujeme pomocí dvou seznamů nezáporných celých čísel.
Každý seznam představuje důlky jednoho z hráčů (postupně zleva doprava
z hráčova pohledu), přičemž počet kuliček v bance hráče je posledním prvkem
seznamu. Desce naznačené výše tedy odpovídají seznamy [A, B, C, D, E, F, G]
a [H, I, J, K, L, M, N].
Abyste si hru mohli vyzkoušet (poté, co úlohu vyřešíte), je vám k dispozici
soubor game_mancala.py, který vložte do stejného adresáře, jako je soubor
s vaším řešením, případně jej upravte dle komentářů na jeho začátku
a spusťte. Kliknutím na jeden z důlků se provede tah, klávesa R hru
resetuje a Q ukončí.
Implementujte nejprve čistou funkci init, která vrátí dvojici seznamů
reprezentujících hrací desku se size menšími důlky, v nichž je na začátku
start kuliček. Banky obou hráčů jsou prázdné. Předpokládejte, že size
i start jsou kladná celá čísla.
def init(size, start):
pass
Dále napište proceduru play, která odehraje jedno kolo hry. Parametr
our je seznam reprezentující stranu aktuálního hráče, parametr their
je seznam reprezentující stranu protivníka. Předpokládejte, že tyto seznamy
mají stejnou délku větší než 1 a že obsahují pouze nezáporná celá čísla.
Parametr position (celé číslo) určuje, který důlek se má vybrat (0 je
důlek nejvíce vlevo z pohledu hráče).
Pokud je position mimo platný rozsah, procedura nic nemodifikuje a vrátí
konstantu INVALID_POSITION. Pokud je position indexem prázdného důlku,
procedura nic nemodifikuje a vrátí konstantu EMPTY_POSITION.
Jinak procedura modifikuje seznamy dle pravidel hry a vrátí buď konstantu
PLAY_AGAIN nebo ROUND_OVER, podle toho, jestli má aktuální hráč hrát
znovu nebo už skončil. Tyto konstanty jsou už definovány; nijak je neměňte.
V tomto domácím úkolu si naprogramujete zjednodušenou variantu hry 2048.
Na rozdíl od původní hry budeme uvažovat jen jednorozměrný hrací plán,
tj. jeden řádek.
Hrací plán budeme reprezentovat pomocí seznamu nezáporných celých čísel;
nuly budou představovat prázdná místa.
Například seznam [2, 0, 0, 2, 4, 8, 0] reprezentuje následující situaci:
Základním krokem hry je posun doleva nebo doprava. Při posunu se všechna
čísla „sesypou“ v zadaném směru, přičemž dvojice stejných číslic se sečtou.
Posunem doleva se tedy uvedený seznam změní na [4, 4, 8, 0, 0, 0, 0].
Abyste si hru mohli vyzkoušet (poté, co úlohu vyřešíte), je vám k dispozici
soubor game_2048.py, který vložte do stejného adresáře, jako je soubor
s vaším řešením, případně jej upravte dle komentářů na jeho začátku
a spusťte. Hra se ovládá šipkami doleva a doprava, R hru resetuje
a Q ukončí.
Napište proceduru slide, která provede posun řádku reprezentovaného
seznamem row, a to buď doleva (pokud má parametr to_left hodnotu True)
nebo doprava (pokud má parametr to_left hodnotu False). Procedura přímo
modifikuje parametr row a vrací True, pokud posunem došlo k nějaké
změně; v opačném případě vrací False.
FreeCell je pasiánsová karetní hra, kterou možná znáte jako součást
operačních systémů jisté společnosti se sídlem v Redmondu. Ve hře se používá
klasický balíček 52 karet se čtyřmi barvami (suits) a třinácti hodnotami
(ranks) od esa po krále. Hrací pole obsahuje:
volná pole (cells) – typicky čtyři, ve variantách jedno až deset,
domácí pole (foundations) – vždy přesně čtyři, do každého z nich se
odkládají karty ve stejné barvě, postupně od esa po krále,
sloupce (cascades) – typicky osm, ve variantách čtyři až deset;
do sloupců se na začátku rozdají všechny karty.
Povolené přesuny karet jsou následující:
je možno přesouvat karty z volných polí a spodní karty sloupců;
na prázdné volné pole a do prázdného sloupce je možno položit libovolnou
kartu;
na prázdné domácí pole je možno položit eso libovolné barvy;
na kartu v domácím poli je možno položit kartu stejné barvy s hodnotou
přesně o jednu vyšší;
na spodní kartu sloupce je možno položit další kartu, pokud je její
hodnota přesně o jednu nižší a pokud se její barva liší (ve smyslu
červená / černá).
Karty budeme reprezentovat jako dvojice (rank, suit), kde rank je
jedno z čísel 1 až 13 (pro karty s hodnotami 1, 11, 12, 13 máme níže
zavedeny konstanty) a suit je jedno z čísel 0 až 3 (postupně reprezentující
srdce, kára, piky a kříže; níže opět reprezentované konstantami).
Zde uvedené konstanty nijak neměňte.
Implementujte predikát can_move, tj. jestli je v zadané situaci možné
provést přesun nějaké karty. Situace je reprezentována třemi seznamy,
jejichž prvky jsou buď karty nebo None.
cascades je seznam spodních karet sloupců (None je prázdný sloupec),
cells je seznam karet na volných polích (None je prázdné pole),
foundation je seznam horních karet na domácích polích (None je opět
prázdné pole).
Předpokládejte, že vstupní situace je skutečnou situací ve hře (např. není
možné, aby se někde objevila stejná karta dvakrát).
V této kapitole se budeme opět zabývat zabudovanými datovými
strukturami: z třetí kapitoly již známe seznam a n-tici, tento
týden přibudou zásobník (stack), slovník (dictionary) a
množina (set).
Přípravy:
attendance – práce s množinou
worktime – práce se slovníkem
sublist – algoritmus nad seznamy čísel
sum – hledání součtu ve dvojici seznamů
course – práce se slovníkem známek
colours – práce se slovníkem barev v reprezentaci RGB
Tato kapitola přidává dva nové typy složených hodnot:9
množina – set – podobně jako seznam obsahuje vnitřní hodnoty,
s tím rozdílem, že v množině nemají hodnoty pevně určené pořadí,
a každá se v dané množině může objevit nejvýše jednou,
slovník – dict – obsahuje klíče (podobně jako v množině se
daný klíč může objevit nejvýše jednou) a ke každému klíči právě
jednu přidruženou hodnotu (obvykle nazýváme prostě hodnota, a
mluvíme o dvojicích klíč – hodnota).
Pro hodnoty, které vkládáme do množin, nebo je používáme jako klíče
ve slovníku, platí důležité omezení: taková hodnota nesmí mít
vnitřní přiřazení, ani jiné operace, které mohou vnitřně danou
hodnotu změnit. Zejména tedy nelze takto používat seznamy, ale ani
slovníky nebo množiny. Přípustná jsou naopak zejména celá čísla,
řetězce a n-tice z nich složené.
S novými typy hodnot přidáváme i nové tvary výrazů (literály,
přístup k přidruženým hodnotám, množinové operace) a příkazů
(přiřazení, for cyklus) a nové zabudované podprogramy.
Jak jsme již zvyklí, hodnoty typu množina a slovník můžeme do
programu zapsat pomocí speciálních výrazů – literálů (podobně jako
tomu bylo u seznamů, n-tic a řetězců). Tyto literály mají tvar:
{} je prázdný slovník (pozor, nikoliv množina!),
{klíč₁: hodnota₁, klíč₂: hodnota₂, …} je slovník, kde klíčᵢ
jsou výrazy, kterých vyhodnocením vzniknou klíče, přičemž
vyhodnocením výrazu hodnotaᵢ vznikne vždy hodnota přidružená
odpovídajícímu klíči (vyhodnotí-li se dva různé výrazy klíčᵢ na
stejný výsledek, použije se dvojice více vpravo),
{hodnota₁, hodnota₂, …} reprezentuje množinu s prvky, které
vzniknou vyhodnocením výrazůhodnotaᵢ.
Prázdná množina literál nemá. Chceme-li vytvořit prázdnou množinu,
použijeme k tomu zabudovanou funkci set() bez parametrů.
Přístup k přidružené hodnotě uložené ve slovníku10 zapisujeme výrazem
tvaru slovník[klíč], kde:
slovník je výraz který se vyhodnotí na hodnotu typu slovník a
klíč je výraz, který je nejprve vyhodnocen, poté je výsledná
hodnota ve slovníku vyhledána,
výraz slovník[klíč] jako celek se pak vyhodnotí na odpovídající
přidruženou hodnotu byl-li klíč ve slovníku nalezen, v opačném
případě je program ukončen s chybou.
Oproti seznamům jsou jak množiny tak slovníky vybaveny efektivním
dotazem na přítomnost prvku (u slovníku klíče), a to výrazy tvaru:
hodnota in množina
klíč in slovník
kde hodnota, množina, klíč a slovník jsou podvýrazy a
výsledkem je pravdivostní hodnota.
Zápis je analogický k indexaci seznamů a řetězců. Oproti těmto již známým typům ale slovníky „indexujeme“ klíčem, který nemusí být celé číslo, a i v případě, kdy jím celé číslo je, nemusí klíče tvořit spojitou řadu začínající nulou. Množinu indexovat nelze.
d.keys() – výsledkem je speciální hodnota, kterou lze pouze
iterovat nebo převést na seznam (viz níže), a která obsahuje
pouze klíče ve slovníku přítomné (bez přidružených hodnot),
d.values() – analogicky, ale pro přidružené hodnoty,
d.items() – taktéž, ale obsahuje dvojice (klíč, hodnota),
d.get(k) nebo d.get(k, fallback) – vyhledá klíč k
v slovníku, a vyhodnotí se na odpovídající hodnotu, je-li tato
přítomna, jinak na None (první tvar) nebo na fallback (druhý
tvar),
d.pop(k) – odstraní ze slovníku klíč k (včetně přidružené
hodnoty),
d.copy() – vytvoří kopii slovníku.
Objekty typu množina pak mají tyto zabudované metody:
s.add(v) – vloží do množiny hodnotu v (byla-li již přítomna,
nestane se nic),
s.remove(v) – odstraní hodnotu v (není-li hodnota přítomna,
program je ukončen s chybou),
Pro vytváření hodnot přidáváme několik zabudovaných čistých
funkcí:
list(x) – převede hodnotu x na seznam, kde x může být:
množina,
výsledek volání d.keys(), d.values() nebo d.items() na
slovníku d,
Pro práci s prvky množin a s klíči, hodnotami a dvojicemi (klíč,
hodnota) ve slovníku lze použít for cykly těchto tvarů:
for vazby in množina:
příkazy
for vazby in slovník.keys():
příkazy
for vazby in slovník.items():
příkazy
for vazby₁, vazby₂ in slovník.items():
příkazy
Kde vazby je vždy buď jméno nebo rozbalení a množina a
slovník jsou výrazy. V posledním uvedeném případě je nutné
případné rozbalení uzávorkovat, například:
for shape, (x, y) in centers.items():
pass
Posledním novým prvkem je vnitřní přiřazení do slovníku:
slovník[klíč] = hodnota
kde slovník, klíč i hodnota jsou výrazy. Byl-li klíč již
ve slovníku přítomen, jeho přidružená hodnota se změní na výsledek
vyhodnocení výrazu hodnota. V opačném případě je klíč do slovníku
přidán (pozor, v tomto se slovníky liší od seznamů).
V tomto příkladu budeme pracovat se systémem docházky jedné
fiktivní firmy. Při příchodu do práce si musí každý zaměstnanec
pípnout kartičkou u vchodu a zaznamenat tak svůj příchod. Při
odchodu zase stejně musí zaznamenat, že z práce odešel.
Čidlo u dveří pak do firemního systému zaznamená data o docházce
zaměstnance. Každý záznam je trojice obsahující kód zaměstnance,
časovou známku a typ záznamu - příchod nebo odchod.
EmployeeId = str # kód zaměstnance
TimeStamp = int # počet sekund od nějakého pevného bodu
RecordType = bool # typ záznamu
Bohužel, někteří zaměstnanci zapomínají zaznamenávat svou
docházku. Vaším úkolem je napsat čistou funkci
employees_with_missing_records, která projde seznam záznamů, a
vrátí množinu obsahující kódy těch zaměstnanců, pro které existuje
v seznamu nějaká nesrovnalost – buď z práce odešli, aniž by do ní
přišli, nebo přišli do práce vícekrát bez záznamu o odchodu.
Seznam záznamů začíná v situaci, kdy žádný zaměstnanec v práci není.
Můžete počítat s tím, že seznam je seřazený podle času od
nejstaršího záznamu po nejnovější.
Na základě odpracovaných hodin za jeden měsíc firma počítá mzdu
pro zaměstnance. Napište čistou funkci seconds_spent_working,
která zjistí, kolik sekund každý zaměstnanec odpracoval. Můžete
počítat s tím, že vstupní seznam je seřazený podle časových známek
od nejstaršího záznamu po nejnovější, že se v něm nevyskytují
žádné nesrovnalosti, že záznamy začínají v situaci, kdy žádný zaměstnanec
v práci není a že každý zaměstnanec, který do práce přišel, z ní také
později odešel.
Nápověda: odečtením dvou časových známek zjistíte, kolik sekund
uplynulo mezi nimi.
V tomto příkladu dostanete dva seznamy obsahující celá čísla.
Vaším úkolem je napsat čistou funkci largest_common_sublist_sum,
která najde takový společný podseznam seznamů left a right,
který má největší celkový součet, a tento součet vrátí.
Podseznamem seznamu S myslíme takový seznam T, pro který
existuje číslo k takové, že platí S[k + i] == T[i] pro všechna
i taková, že . Například seznam [1, 2] je
podseznamem seznamu [0, 1, 2, 3], kde k = 1.
Složitost smí být v nejhorším případě až kubická vzhledem k délce
delšího vstupního seznamu.
Vaším prvním úkolem je napsat predikát sum_to_exactly, který
rozhodne, zda se v seznamu left nachází nějaký prvek x a
v seznamu right nějaký prvek y tak, že platí x + y == to.
Řešení, kde bude počet kroků výpočtu úměrný součinu délek obou
seznamů, je vyhovující.11
def sum_to_exactly(left: list[int], right: list[int], to: int) -> bool:
pass
Dále napište predikát sum_to_at_least, který rozhodne, zda se
v seznamu left nachází nějaký prvek x a v seznamu right
nějaký prvek y tak, že platí x + y >= at_least. V tomto případě
vyžadujeme složitost lineární vzhledem k délce delšího seznamu.
Známky studentů z jednoho předmětu jsou uloženy ve slovníku, kde
klíčem je UČO studenta a hodnotou je známka zadaná jako písmeno.
Možná hodnocení jsou 'A' až 'F', dále, 'N', 'P', 'X', 'Z' a '-'.
Napište čistou funkci modus, jejímž vstupem bude slovník známek
a výstupem bude jejich modus, tedy nejčastější hodnota.
Předpokládejte, že známek se stejnou četností může být více, takže
funkce bude vždy vracet množinu známek, a to i v případě, že je
nejčastější hodnota určena jednoznačně. V případě, že je vstupní
slovník prázdný, bude výstupem prázdná množina.
Dále napište predikát check, který ověří, že známky jsou
smysluplné, tedy že odpovídají buďto předmětu ukončenému zkouškou
(známky 'A' - 'F', nebo 'X'), kolokviem (známky 'P' nebo 'N'),
anebo zápočtem (známky 'Z' nebo 'N'). Hodnocení '-' je možné
u jakéhokoliv způsobu hodnocení. Klasifikované zápočty
neuvažujeme.
V tomto příkladu budeme pracovat s RGB kódy různých barev. Tyto
kódy jsou uloženy ve slovníku, kde klíčem je řetězec - název
barvy, a hodnota je trojice celých čísel, které představují
hodnoty červené, zelené a modré složky.
Vaším úkolem je napsat čistou funkci, která na vstupu dostane
slovník barev a trojici celých čísel z rozsahu 0–255 a vrátí
množinu názvů, které jsou zadané trojici nejblíže (množina bude
obsahovat více prvků pouze v případě, že několik různých barev je
od té zadané stejně daleko).
Blízkost barev budeme měřit pomocí tzv. Manhattanské vzdálenosti,
která je dána součtem absolutních hodnot rozdílů na jednotlivých
souřadnicích. Například pro trojice
Binární relací nad danou množinou je množina dvojic prvků z této
množiny. Daná relace se pak nazývá tranzitivní, platí-li pro
všechny dvojice z této relace, že se v relaci
nachází i dvojice . V této úloze budeme pracovat
s relacemi nad celými čísly.
Napište predikát, který rozhodne, je-li zadaná relace tranzitivní.
Vaším úkolem bude naprogramovat základní množinové operace (zatím
máme k dispozici pouze operace, které pracují vždy s jedním
prvkem). U každé operace si rozmyslete, kolik kroků provede
vzhledem k velikostem obou vstupních množin.
První a v nějakém smyslu nejjednodušší operací je sjednocení.
Nejprve implementujte sjednocení jako čistou funkci, poté jako
proceduru, která rozšíří stávající množinu o prvky nějaké další (a
implementuje tedy sjednocení „in situ“). Srovnejte jejich
složitost.
Druhou standardní operací je průnik. Ten je o něco složitější
a také je na místě zvážit rozdíl mezi čistou verzí, která sestrojí
novou množinu, a procedurou, která zmenší množinu stávající. Dejte
pozor na to, že tu stejnou množinu není dovoleno zároveň jak měnit
tak procházet.
Množinový rozdíl má jednu zajímavou variaci – tzv. symetrický
rozdíl, kdy konstruujeme množinu, která obsahuje prvky, které
náleží do právě jedné vstupní množiny. Opět implementujte obě
verze. Symetrický rozdíl je možné složit z ostatních množinových
operacích mnoha způsoby – rozmyslete si, které fungují lépe a
které hůře.
V tomto příkladu budeme pracovat se slovníky. Slovník může mimo
jiné reprezentovat zobrazení: klíč se zobrazí na příslušnou
hodnotu. Naprogramujte čistou funkci image, které předáme
slovník f, který reprezentuje zobrazení, a množinu values.
Výsledkem bude obraz množiny values – tedy množina hodnot, na
které se hodnoty z množiny values zobrazí.
Dále naprogramujte čistou funkci compose, které vstupem budou
dvě zobrazení (slovníky) f a g a výsledkem bude slovník, který
reprezentuje zobrazení f ∘ g. Vstupní podmínkou je, že f je
definováno pro každou hodnotu z obrazu g.
Konečně naprogramujte čistou funkci kernel, které vstupem bude
zobrazení (slovník) f a výsledkem bude relace ekvivalence
(množina dvojic) taková, že právě když .
Vaším úkolem je naprogramovat tzv. „hru života“ – jednoduchý
dvourozměrný celulární automat. Simulace běží na čtvercové síti,
kde každá buňka je mrtvá (hodnota 0) nebo živá (hodnota 1).
V každém kroku se přepočte hodnota všech buněk, a to podle toho,
zda byly v předchozím kroku živé a kolik měly živých sousedů
(z celkem osmi, tzn. včetně úhlopříčných):
stav
živí sousedé
výsledek
živá
0–1
mrtvá
živá
2–3
živá
živá
4–8
mrtvá
mrtvá
0–2
mrtvá
mrtvá
3
živá
mrtvá
4-8
mrtvá
Příklad krátkého výpočtu:
Jiný (periodický) výpočet je například:
Napište čistou funkci, která dostane jako parametry počáteční stav
hry (jako množinu dvojic, která reprezentuje souřadnice živých
buněk) a počet kroků, a vrátí stav hry po odpovídajícím počtu
kroků.
Budeme zkoumat řadu vedle sebe sedících světlušek. Každá světluška
má energii, která se vyjadřuje nezáporným celým číslem. Bude nás zajímat
vývoj této energie v čase, přičemž v každém kroku dojde k následujícímu:
Energie všech světlušek se zvětší o 1.
Světlušky, které mají energii větší než 3, se rozsvítí. To způsobí,
že se energie jejich sousedních světlušek zvýší o další 1.
To může způsobit jejich rozsvícení (pokud dosud nebyly rozsvícené) atd.
Energie všech světlušek, které se v tomto kroku rozsvítily, se sníží
na 0. Všechny rozsvícené světlušky zhasnou.
Máme-li tedy na začátku světlušky ve stavu [0, 2, 0, 2, 0],
v následujícím kroku budou ve stavu [1, 3, 1, 3, 1] a dále pak
[3, 0, 0, 0, 3].
Čistá funkce light_bugs vrátí seznam seznamů reprezentujících
prvních time kroků pozorování světlušek, jejichž počáteční
energie je daná parametrem start. Předpokládejte, že se start
skládá jen z čísel od 0 do 3 včetně, má délku alespoň dvě a že
time je kladné celé číslo.
def light_bugs(start, time):
pass
Příklad: pro vstup ([0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0], 7)
funkce vrátí následující seznam:
Nyní přidáme analogické dotazy tohoto tvaru na přítomnost hodnoty
v seznamu: hodnota in seznam (zde seznam je opět podvýraz), ale
musíme si pamatovat, že pro seznam tento dotaz není efektivní:
obsahuje skrytou iteraci potenciálně všemi prvky seznamu.
Pro množiny připouštíme nově tyto tvary výrazů (kde množina₁ a
množina₂ jsou vždy podvýrazy, které se musí vyhodnotit na
hodnoty typu množina):
množina₁ | množina₂ se vyhodnotí na sjednocení,
množina₁ & množina₂ se vyhodnotí na průnik a
množina₁ - množina₂ se vyhodnotí na rozdíl příslušných
množin.
Konečně pro seznamy přidáváme výraz tvaru seznam₁ + seznam₂ (kde
seznam₁ a seznam₂ jsou opět podvýrazy), který se vyhodnotí na
nový seznam s prvky z prvního i druhého seznamu (nejprve všechny
prvky levého operandu, pak všechny prvky pravého, vždy v původním
pořadí).
Objekty typu množina získají tyto nové zabudované metody:
s₁.update(s₂) – přidá do množiny s₁ všechny prvky, které se
nachází v s₂ (v s₁ tak bude po provedení operace sjednocení
obou množin),12
s₁.intersection_update(s₂) – odebere z množiny s₁ všechny
prvky, které se nenachází v s₂ (v s₁ tedy bude po provedení
průnik),
s₁.difference_update(s₂) – odebere z množiny s₁ všechny
prvky, které se nachází v s₂ (v s₁ tedy bude po provedení
rozdíl).
Přidáme také několik zabudovaných metod pro práci se seznamy.
Pozor všechny tyto metody jsou ekvivalentní iteraci – nelze tedy
jejich použitím ušetřit výpočetní čas, jsou jen syntaktickou
zkratkou pro obšírnější for cyklus:
l.reverse() – otočí pořadí prvků v seznamu,
l.index(v) – vyhodnotí se na index, na kterém se nachází
hodnota v (je-li takových více, výsledkem je ten nejmenší;
není-li takový žádný, program je ukončen s chybou),
l₁.extend(l₂) – přidá na konec seznamu l₁ všechny prvky ze
seznamu l₂ (ve stejném pořadí),
l.insert(i, v) – vloží před index i hodnotu v (tedy
hodnoty na indexech j ≥ i přesune o jednu pozici doprava a na
index i uloží hodnotu v),
l.pop(i) – odstraní hodnotu z indexu i (a tedy všechny
hodnoty na vyšších indexech přesune o jednu pozici doleva).
Pozor, s₁.update(s₂)není totéž, jako s₁ = s₁ | s₂ – první operace vnitřně změní existující hodnotu s₁, ta druhá vytvoří novou množinu a výsledek sváže se jménem s₁.
Uvažme následovný problém: na vstupu máme výškový profil trasy, a
zajímá nás, jak dlouho jsme se pohybovali ve výšce aspoň takové,
v jaké jsme teď. Zajímavé hodnoty budeme samozřejmě dostávat pouze
na sestupu. Například (aktuální pozici budeme značit symbolem × a
odpovídající úsek vyšší nadmořské výšky vybarvíme):
Definujeme tedy čistou funkci hills, která dostane na vstupu
seznam výšek (celých čísel) a které výsledkem bude stejně dlouhý
seznam indexů, které odpovídají vždy prvnímu vybarvenému sloupci
v ilustraci výše.
def hills(heights: list[int]) -> list[int]:
V proměnné stack budeme udržovat zásobník, který bude
obsahovat indexy všech předchozích vrcholů, které jsou nižší
než ten aktuální. Do proměnné indices budeme počítat
výsledný seznam indexů.
stack: list[int] = []
indices: list[int] = []
for i in range(len(heights)):
while len(stack) > 0 and heights[stack[-1]] >= heights[i]:
stack.pop()
if len(stack) == 0:
indices.append(0)
else:
indices.append(stack[-1] + 1)
stack.append(i)
return indices
Funkčnost ověříme na několika příkladech (seznam example
odpovídá obrázku výše).
V této ukázce se budeme zabývat datovým typem množina. Stejně
jako u seznamů, slovníků a podobně se jedná o složený typ, který
má prvky. Množina má některé vlastnosti společné jak se seznamem –
obsahuje pouze prvky, ale nikoliv klíče, tak se slovníkem –
podobně jako klíče ve slovníku, hodnoty v množině můžou být
přítomny nejvýše jednou. Od seznamu se liší mimo jiné tím, že
množinu nelze indexovat (pouze iterovat).
Krom omezení na výskyt každého prvku nejvýše jednou poskytuje
množina efektivní test na přítomnost prvku (podobně, jako
slovník poskytuje efektivní test na přítomnost klíče). Chceme-li
zjistit, objevuje-li se nějaká hodnota v běžném seznamu, strávíme
tím čas, který je přímo úměrný počtu prvků tohoto seznamu. Naopak
v množině lze očekávat, že čas potřebný pro zjištění přítomnosti
na počtu prvků v množině vůbec nezávisí: trvá přibližně stejně
dlouho nalézt prvek v množině o deseti prvcích i v množině
o deseti milionech prvků (takto to funguje v Pythonu – tato
operace má očekávanou konstantní složitost; některé jiné jazyky
poskytují datový typ množina, kde čas potřebný k zjištění
přítomnosti prvku závisí na tom, kolik řádů má číslo popisující
její velikost – mluvíme pak o tzv. logaritmické složitosti).
Uvažme zobrazení kde a je zadané
tabulkou (slovníkem, kde klíč je dvojice čísel a hodnota je číslo
– rozmyslete si, že takový slovník skutečně reprezentuje tabulku,
budou-li ve slovníku přítomny všechny potřebné dvojice).
Například logickou spojku and lze podobnou tabulkou
reprezentovat takto (budeme-li reprezentovat True číslem 1 a
False číslem 0):
0
1
0
0
0
1
0
1
Jako slovník bychom stejnou tabulku zapsali takto:
{(0, 0): 0, (0, 1): 0,
(1, 0): 0, (1, 1): 1}.
Zobrazení budeme říkat operace a budeme jej popisovat
následujícím typem:
Operation = dict[tuple[int, int], int]
Na vstupu tedy dostaneme tabulku, která reprezentuje a množinu
čísel . Naším úkolem bude nalézt nejmenší množinu čísel
takovou, že:
, tedy C obsahuje všechny zadané prvky,
pro každé platí – říkáme, že
množina je uzavřena na operaci .
Jak budeme postupovat? Množinu budeme budovat postupně:
začneme tím, že do vložíme všechny prvky z :
set_c = set_b.copy()
Dále budeme procházet všechny dvojice ze součinu , a
nalezneme-li takovou, že její obraz ještě v množině není,
přidáme jej tam. Toto ale nemůžeme udělat přímo: přidat prvek
do množiny, kterou právě iterujeme, je zakázáno (protože by
bylo těžké zaručit, aby byla iterace konzistentní – tzn. aby
se nestalo, že v iteraci uvidíme některé, ale ne všechny, nové
prvky).
Proto si napíšeme pomocnou funkci find_missing, která najde
chybějící prvky a vrátí je jako množinu. Stojíme před dvěma
problémy: po přidání nových prvků musíme celou proceduru
opakovat, protože vznikly nové dvojice. Tento problém vyřešíme
tak, že budeme funkci find_missing volat opakovaně, tak
dlouho, dokud bude nalézat nové prvky.
Druhý problém je, že tento postup není příliš efektivní: rádi
bychom se vyhnuli procházení dvojic, které jsme již
kontrolovali. To sice samozřejmě lze, ale značně by nám to
zkomplikovalo kód, proto tentokrát ušetříme práci sobě (a
nějakou tím přiděláme počítači).
to_add = find_missing(set_c, operation_f)
while len(to_add) != 0:
set_c.update(to_add)
to_add = find_missing(set_c, operation_f)
return set_c
Pomocná (čistá) funkce find_missing je velmi jednoduchá: projde
všechny dvojice z (tedy součinu množiny set_c se sebou
samou), a zobrazí-li se tato dvojice na prvek, který v set_c
zatím není, přidá ho do své návratové hodnoty.
Jak jistě víte, binární relací nad danou množinou je každá
množina dvojic prvků z množiny , tzn. relace nad je
podmnožina kartézského součinu . Daná relace se pak nazývá
symetrická, platí-li pro všechny dvojice z této relace,
že se v relaci zároveň nachází i dvojice . V této úloze
budeme pracovat s relacemi nad celými čísly.
Napište predikát, kterého hodnota bude True dostane-li
v parametru symetrickou relaci, False jinak.
neprázdný výraz expr složený z proměnných a z aritmetických
operátorů, zapsaný v postfixové notaci, a
slovník, přiřazující proměnným číselnou hodnotu (můžete se
spolehnout, že všechny proměnné použité v daném výrazu jsou
v tomto slovníku obsaženy),
a vrátí číslo, na které se daný výraz vyhodnotí. Každý operátor
nebo proměnná je samostatný řetězec, celý výraz je pak tvořen
posloupností těchto řetězců. Povolené operátory jsou pouze + a
*.
Postfixová notace funguje následovným způsobem:
výraz čteme zleva doprava, přitom si každou hodnotu zapíšeme,
narazíme-li na operátor, např. +:
v hlavě sečteme poslední dvě hodnoty které jsme napsali,
tyto hodnoty smažeme,
zapíšeme místo nich součet, který jsme si zapamatovali.
Tento postup opakujeme, až dokud nepřečteme celý výraz. Je-li
výraz správně utvořený, na konci tohoto procesu máme zapsané
jediné číslo. Toto číslo je výsledkem vyhodnocení zadaného výrazu.
Dané přirozené číslo je b–šťastné platí-li, že nahradíme-li jej
součtem druhých mocnin jeho cifer, vyjádřených v poziční soustavě
se základem b, a tento postup budeme dále opakovat na takto
vzniklém čísle, po konečném počtu kroků dostaneme číslo 1.
Například číslo 3 je 4–šťastné, protože:
.
Číslo 2 není 5–šťastné:
a protože se nám ve výpočtu číslo 4 zopakovalo, nemůžeme již dojít
k výsledku 1.
Napište predikát, který o číslu number rozhodne, je-li
base-šťastné.
Flood fill je algoritmus z oblasti rastrové grafiky, který
vyplní souvislou jednobarevnou plochu novou barvou. Postupuje
tak, že nejdříve na novou barvu obarví pozici, na které začíná,
dále se pokusí obarvit její sousedy (pozice jiné než cílové barvy
se neobarvují), a podobně pokračuje se sousedy těchto sousedů,
atd. Zastaví se, dojde-li na okraj obrázku, nebo narazí na pixel,
který nemá žádné nové stejnobarevné sousedy.
Sousední pixely uvažujeme pouze ve čtyřech směrech, tj. ne
diagonálně.
Napište proceduru, která na vstupu dostane plochu reprezentovanou
obdélníkovým seznamem seznamů (délky všech vnitřních seznamů jsou
stejné), počáteční pozici (je zaručeno, že se bude jednat o platné
souřadnice), a cílovou barvu, na kterou mají být vybrané pozice
přebarveny.
Napište (čistou) funkci, která na vstupu dostane signál data
reprezentovaný seznamem celočíselných amplitud (vzorků).
Výsledkem bude statistika tohoto signálu, kterou vytvoří
následujícím způsobem:
funkce signál nejdříve očistí od všech vzorků s amplitudou
větší než max_amplitude a menších než min_amplitude,
následně jej převzorkuje tak, že sloučí každých bucket
vzorků (poslední vzorek může být nekompletní) do jednoho
vypočtením jejich průměru a jeho následným zaokrouhlením
(pomocí vestavěné funkce round),
nakonec spočítá, kolikrát se v upraveném signálu objevují
jednotlivé amplitudy, a vrátí slovník, kde klíč bude amplituda
a hodnota bude počet jejích výskytů.
V této úloze budete zjišťovat, je-li možné pomocí alchymie vyrobit
požadovanou substanci. Vstupem je:
množina substancí, které již máte k dispozici (máte-li už
nějakou substanci, máte ji k dispozici v neomezeném množství),
slovník, který určuje, jak lze existující substance
transmutovat: klíčem je substance kterou můžeme vytvořit a
hodnotou je seznam „vstupních“ substancí, které k výrobě
potřebujeme,
cílová substance, kterou se pokoušíme vyrobit.
Napište predikát, kterého hodnota bude True, lze-li z daných
substancí podle daných pravidel vytvořit substanci požadovanou,
False jinak.
Čistá funkce valid_stack_ops dostane na vstupu dva seznamy
pushed, popped a rozhodne, jestli tyto seznamy mohly být
výsledkem posloupnosti operací push a pop nad zásobníkem,
který je na začátku prázdný. (Seznam pushed má odpovídat pořadí,
v němž byly prvky vkládány operací push; seznam popped pořadí,
v němž byly prvky odebírány operací pop.) Předpokládejte, že se
ani v jednom vstupním seznamu neopakují stejné prvky.
Příklady:
Pro vstup ([1, 2, 3, 4, 5], [4, 5, 3, 2, 1]) má být výsledkem
True, protože existuje posloupnost operací push 1, push 2,
push 3, push 4, pop (vrátí 4), push 5, pop (vrátí 5),
pop (vrátí 3), pop (vrátí 2), pop (vrátí 1).
Pro vstup ([1, 2, 3, 4, 5], [4, 3, 5, 1, 2]) má být výsledkem
False, protože neexistuje žádná posloupnost operací push
a pop, která by odpovídala těmto seznamům.
Mějme funkci f, která pro dané celé číslo a vrátí množinu
obsahující a, a // 2 a a // 7. Použitím této funkce na
množině pak míníme její použití na každém prvku dané množiny a
následné sjednocení všech obdržených výsledků.
Napište (čistou) funkci, která na množinu ze svého argumentu
použije f, dále použije f na obdržený výsledek a takto bude
pokračovat až dojde do bodu, kdy se dalším použitím f daná
množina už nezmění. Výsledkem bude počet aplikací f na množinu,
které bylo potřeba provést, než se proces zastavil.
Například z množiny {1, 5, 6} vznikne první aplikací popsané
funkce množina {0, 1, 2, 3, 5, 6}:
hodnota 1 se zobrazila na {1, 1 // 2 = 0, 1 // 7 = 0},
hodnota 5 na {5, 5 // 2 = 2, 5 // 7 = 0}, a konečně
hodnota 6 na {6, 6 // 2 = 3, 6 // 7 = 0}.
Po další aplikaci se už množina nijak nezmění, proto je výsledkem
číslo jedna.
Tedy klíče jsou čísla vrcholů a hodnoty jsou seznamy jejich
(přímých) potomků. Napište čistou funkci, která najde „nejdelší
řádek“ v obrázku takovéhoto stromu a vrátí jeho délku. Řádek je
vždy tvořen uzly, které mají stejnou vzdálenost od kořene.
Pomůcka: máte-li uložený nějaký řádek v seznamu, lehce získáte
řádek následující (o jedna vzdálenější od kořene). Pak už stačí
nalézt nejdelší takový seznam.
Uvažujme jednoduché aritmetické výrazy se sčítáním a násobením.
Budeme je ukládat do dvojice slovníků (expr a const), a to
následovně:
klíč je vždy jméno proměnné (řetězec),
hodnota ve slovníku expr je trojice:
první složka je operátor '*' nebo '+',
druhá a třetí složka jsou operandy – názvy proměnných,
hodnota ve slovníku const je číslo.
Každá proměnná se objeví v nejvýše jednom slovníku. Proměnné,
které se nenachází v žádném z nich jsou rovny nule.
Napište čistou funkci, která dostane jako parametry slovníky
expr a const a název proměnné. Výsledkem bude hodnota této
proměnné. Při vyhodnocování se Vám bude hodit zásobník a pomocný
slovník.
† Uvažme městskou hromadnou dopravu, která má pojmenované zastávky,
mezi kterými jezdí (pro nás anonymní) spoje. Spoje mají daný směr:
není zaručeno, že jede-li spoj z do , jede i spoj z do
. Dopravní síť budeme reprezentovat slovníkem, kde klíčem je
nějaká zastávka , a jemu příslušnou hodnotou je seznam
zastávek, do kterých se lze z dopravit bez dalšího zastavení.
Napište predikát, který rozhodne, je-li možné dostat se
z libovolné zastávky na libovolnou jinou zastávku pouze použitím
spojů ze zadaného slovníku.
† Napište (čistou) funkci, která na vstupu dostane průřez krajiny a
spočte, kolik vody se v dané krajině udrží, bude-li na ni
neomezeně pršet. Krajina je reprezentována sekvencí celých
nezáporných čísel, kde každé reprezentuje výšku jednoho úseku.
Všechny úseky jsou stejně široké a mimo popsaný úsek krajiny je
všude výška 0.
Například krajina [3, 1, 2, 3, 2] dokáže udržet 3 jednotky vody
(mezi prvním a čtvrtým segmentem):
Představte si robota, který se umí pohybovat dopředu a dozadu a otáčet
se o 90° v obou směrech. Pozici robota reprezentujeme dvojicí celých čísel;
první souřadnice je -ová (záporná čísla jsou na západ od
počátku, kladná na východ), druhá souřadnice je -ová (záporná
čísla jsou na sever, kladná na jih).
Čistá funkce simulate_robot dostane seznam instrukcí pro robota,
vykoná je a vrátí finální pozici robota. Na začátku je robot na
souřadnicích (0, 0) a je otočen směrem k severu. Jednotlivé
instrukce jsou dvojice v tomto formátu:
("rotate", n) – robot se otočí o doprava (pro záporná
doleva);
("forward", n) – robot se posune o kroků dopředu;
("backward", n) – robot se posune o kroků dozadu;
("undo", n) – robot zruší efekt posledních provedených
instrukcí.
U příkazů jiných než rotate je n vždy nezáporné celé číslo.
Instrukce undo může být použita vícekrát a je tak možno rušit
efekt více instrukcí, např. posloupnost instrukcí forward 3,
backward 7, undo 1, undo 1 způsobí, že robot bude stát na
své počáteční pozici. Smíte předpokládat, že k instrukci undo n
nedojde ve chvíli, kdy zbývá méně než předchozích instrukcí.
Zejména tedy undo 1 nemůže stát na začátku souboru (ale undo 0
ano).
Představte si robotickou žábu, která umí skákat rovně dopředu
o zadanou celočíselnou délku a otáčet se o 90° v obou směrech.
Čistá funkce simulate_frogbot dostane seznam instrukcí pro robožábu,
vykoná je a vrátí počet různých pozic, na kterých se žába během vykonávání
instrukcí nacházela (včetně počáteční a poslední pozice). Pozor na to,
že na některou pozici se v průběhu vykonávání instrukcí může žába dostat
vícekrát – tuto pozici pořád započítáváme jen jednou.
Jednotlivé instrukce jsou dvojice v tomto formátu:
("rotate", n) – robožába se otočí o (kladný úhel
doprava, záporný doleva);
("jump", n) – robožába poskočí o jednotek dopředu.
Zde může být libovolné kladné celé číslo (funkce musí bez
problémů fungovat i pro obrovská čísla).
Poznámka: Všimněte si, že na počáteční pozici ani natočení žáby odpověď
vůbec nezáleží.
Tato kapitola přináší možnost definovat vlastní (uživatelské) datové
typy. K tomuto účelu zavedeme nový typ definice. Definice datového
typu musí stát vně jakékoliv jiné definice (tedy na stejné úrovni
jako definice funkcí, které jsme doteď znali).
Definice typu má následovný tvar:
class Třída:
def __init__(self, param₁: typ₁, …, paramₙ: typₙ) -> None:
tělo
def metoda₁(self, param₁: typ₁, …, paramₙ: typₙ) -> typ:
tělo
…
Uvnitř definice typu se tedy může objevit definice inicializační
funkce a definice metod (a nic jiného). Tyto definice se v obou
případech velmi podobají na definice funkcí – základním rozdílem
(krom toho, kde stojí) je povinný první parametr s názvem self.
V případě inicializační funkce (povinně nazvané __init__)
reprezentuje parametr self nový objekt, který je potřeba
inicializovat (zejména nastavit počáteční hodnoty atributů).
Nové hodnoty uživatelského typu Třída se vytvoří následovným
výrazem:
Třída(výraz₁, …, výrazₙ)
Protože se jedná o výraz, lze jej použít jako podvýraz v jiných
výrazech, nebo třeba v přiřazovacím příkazu na pravé straně takto:
objekt = Třída(výraz₁, …, výrazₙ)
Tento výraz krom samotného vytvoření objektu zavolá inicializační
funkce __init__, s následovnými vazbami formálních parametrů:
Hlavním úkolem inicializační funkce je nastavit počáteční hodnoty
atributů nového objektu. Atributy se velmi podobají proměnným,
nejsou ale svázané s aktuálně vykonávanou funkcí, ale s objektem.
Přístup k atributům objektu je výraz, který se podobá na použití
metody. Např.:
Objekty mají určitou podobnost s n-ticemi, které již dobře známe:
sdružují několik hodnot (potenciálně různých typů) do jedné. Mají
ale i dvě zásadní odlišnosti:
atributy objektů jsou pojmenované (jsou určeny jmény, nikoliv
pořadím),
objekty mají vnitřní přiřazení – vazbu atributu na hodnotu lze
měnit (použitím přiřazovacího příkazu).
Přiřazení do atributu je příkaz, který se podobá na ostatní druhy
přiřazení, které známe (zejména na vnitřní přiřazení do seznamu nebo
slovníku):
objekt.atribut = výraz
kde objekt a atribut jsou jména. Významem je změna vazby
atributu (na hodnotu, která vznikne vyhodnocením výrazu výraz).
V metodách parametr self reprezentuje objekt, na kterém byla
metoda použita. Tedy při použití metody (druh výrazu, který již
známe u zabudovaných typů):
objekt.metoda₁(výraz₁, …, výrazₙ)
se vážou formální parametry na skutečné parametry takto:
V této ukázce demonstrujeme základní použití složených datových
typů. Srovnejte 05/shapes.py – budeme nyní řešit stejné
problémy, ale místo n-tic (kde jsou jednotlivé složky číslované
ale jinak anonymní) budeme používat složené typy, které mají
jednotlivé složky pojmenované.
from math import isclose, pi, sqrt, cos, sin
Jako první si definujeme typ pro kruh (anglicky disc), který má
jediný atribut, totiž poloměr typu float.
Elipsa reprezentuje podobný případ, kdy potřebujeme k jejímu
popisu dvě čísla, tentokrát délky jejích dvou poloos. Všimněte si,
že na rozdíl od reprezentace v ukázce 05/shapes.py (kde jsme
používali n-tice) nám tu záměna elipsy a obdélníku v žádném
případě nehrozí.
class Ellipse:
def __init__(self, major: float, minor: float) -> None:
assert major >= minor
self.major = major
self.minor = minor
Atributy složeného typu samozřejmě nemusí být všechny stejného
typu (jako tomu bylo v této ukázce dosud). Zadefinujeme si tedy
ještě pravidelný n-úhelník, který zadáme hlavním poloměrem (tzn.
vzdáleností vrcholu od středu) a počtem vrcholů (který je na
rozdíl od poloměru celočíselný).
Dále napíšeme funkci, která ze seznamu obdélníků vybere ten
s největší plochou, existuje-li takový právě jeden. Je zde vidět,
že se složenými typy pracujeme velmi obdobně jako s těmi
zabudovanými. Tím, že používáme pouze abstraktní operace (které
jsou „schované“ do funkcí) je dokonce tělo oproti implementaci
z ukázky 05/shapes.py zcela nezměněné.
for r in rectangles:
if isclose(rectangle_area(r), rectangle_area(largest)):
count += 1
elif rectangle_area(r) > rectangle_area(largest):
count = 1
largest = r
Jak již bylo naznačeno, problém, který se nám objevil
s elipsou a obdélníkem před dvěma týdny nás už nyní nemusí
trápit. Odkomentujete-li následovné tvrzení, mypy Vám
v programu ohlásí chybu.
V této ukázce se budeme zabývat jednoduchými objekty, které můžeme
chápat jako rozšíření složených typů o metody. Metoda je
podprogram, který je svázán se svým složeným typem (objektem):
metoda má vždy parametr, který reprezentuje instanci objektu se
kterou bude pracovat. V Pythonu tento parametr explicitně uvádíme
v hlavičce metody (tzn. v seznamu formálních parametrů), a to vždy
jako první a vždy se jménem self.
Při volání metod používáme tečkovou notaci, stejně jako
u zabudovaných typů: máme-li hodnotu items typu list, můžeme
napsat třeba items.append(1) a víme, že toto volání provede
nějakou akci nad hodnotou items. Naše metody se budou chovat
stejně (ve skutečnosti je totiž append metoda třídy list).
Máme-li hodnotu hospital typu Hospital (u objektů také mluvíme
o instanci hospital třídy Hospital), můžeme napsat třeba
hospital.add_doctor('dept', doc). Metodě definované jako
add_doctor(self, department, doctor) bude hodnota hospital
předána právě parametrem self, hodnoty uvedené při volání
v závorkách pak v dalších parametrech. Přesněji:
hodnota hospital bude předána prvním parametrem (self),
druhý parametr, department, bude mít hodnotu 'dept',
třetí parametr, doctor, bude mít hodnotu doc.
Třída Doctor je obyčejný složený typ bez metod, jaké známe
z předchozí ukázky. Bude mít atributy name (jméno lékaře) a
night_shift (lze-li tomuto lékaři plánovat noční směny).
class Doctor:
def __init__(self, name: str, night_shift: bool) -> None:
self.name = name
self.night_shift = night_shift
Třída Hospital reprezentuje samotnou nemocnici. Nemocnice má
lékaře a oddělení, na kterých jednotliví lékaři pracují. Data
budeme ukládat do slovníku, ve kterém jako klíče použijeme názvy
jednotlivých oddělení a hodnoty budou seznamy lékařů.
class Hospital:
Inicializační funkce __init__ inicializuje novou nemocnici.
Krom objektu, který bude inicializovat (parametr self) jí
předáme seznam názvů oddělení (parametr departments).
Metoda inicializuje atribut departments.
def __init__(self, departments: list[str]) -> None:
self.departments: dict[str, list[Doctor]] = {}
for name in departments:
self.departments[name] = []
Metoda add_doctor zařadí lékaře doctor na oddělení
department. Vstupní podmínkou je, že toto oddělení
v nemocnici existuje.
Protože krom zvláštního zápisu volání je metoda podprogram
jako každý jiný, lze metody stejně tak klasifikovat na čisté
funkce, predikáty a podobně. Není ale obvyklé mluvit v tomto
kontextu o procedurách: metody velmi často mění předaný objekt
(parametr self) – na rozdíl od funkcí budeme tedy
předpokládat, není-li uvedeno jinak, že metoda mění objekt
self.
Budeme nicméně nadále explicitně uvádět, má-li mít metoda
nějaké jiné vedlejší efekty. Není-li tedy uvedeno jinak,
metoda může měnit pouze objekt předaný parametrem self.
Metoda, která je označená jako čistá (a tedy i metoda, která
je označená jako predikát) nemění ani tento.
Metoda (predikát) night_coverage zkontroluje, že je na
každém oddělení aspoň jeden lékař, který může být zařazen na
noční směnu.
def night_coverage(self) -> bool:
for department, doctor_list in self.departments.items():
found = False
for doctor in doctor_list:
if doctor.night_shift:
found = True
break
V této ukázce se zaměříme na datové struktury. Jednoduše zřetězený
seznam jste již viděli v přednášce, zde si ukážeme velice
jednoduchou obměnu téhož. Seznamy tohoto typu nejsou sice v praxi
až tak oblíbené (s možnou výjimkou Linuxového jádra, kde se
používají často) ale velmi dobře ilustrují klíčové znalosti práce
s pamětí. Proto je velmi důležité, abyste jim rozuměli.
Zřetězený seznam je složený z uzlů. Každý uzel je samostatná
hodnota uložená v paměti (to, kde přesně je uložená a jak se o tom
rozhodne, nás prozatím nebude příliš zajímat, stejně jako jsme to
dosud neřešili u jiných typů hodnot). Každý uzel si bude pamatovat
jedno z čísel, které bylo do seznamu uloženo. Co je ale mnohem
zajímavější je, že si zároveň bude pamatovat svého následovníka:
další uzel v seznamu.
Zde je na místě připomenout, jak v Pythonu fungují proměnné,
konkrétně atributy složených typů. Ze třetí kapitoly si jistě
pamatujete, že zabudovaný typ list přiřazuje (váže) hodnoty
k jednotlivým indexům. Má navíc tzv. vnitřní přiřazení: vazbu
indexu a hodnoty lze změnit. Vnitřní přiřazení zapisujeme třeba
items[3] = 9, jeho efekt jsme si ukazovali na obrázku, který si
zde připomeneme:
Složené typy mají stejný koncept vnitřního přiřazení, místo
(proměnného) počtu indexů mají ale (pevnou) množinu jmen.
Zadefinujme si složený typ Node, kterým budeme reprezentovat
jednotlivé uzly zřetězeného seznamu:
Vytvoříme-li novou hodnotu typu Node, například voláním
a = Node(3), bude výsledek vypadat takto:
Vytvořme nyní novou hodnotu, b = Node(5) a použijme vnitřní
přiřazení a.next = b. Výsledek bude:
Pro jistotu vytvoříme ještě jeden uzel, tentokrát dvojicí příkazů
b.next = Node(7) a b = b.next. Výsledná situace bude vypadat
takto:
Na tomto posledním obrázku je také vidět, že k uzlu s hodnotou 5
již sice nemáme přímý přístup (není přímo uložen v žádné
proměnné), dostaneme se k němu ale skrz atribut next uzlu a.
Nyní již můžeme přistoupit k implementaci samotného zásobníku.
Tento bude mít pouze 2 metody, push a pop. Metoda push vloží
novou hodnotu na vrchol zásobníku. Pro tuto hodnotu vytvoří nový
uzel a přidá ho na začátek seznamu. Metoda pop naopak uzel
odstraní a hodnotu v něm uloženou vrátí. Je-li seznam prázdný,
vrátí None.
class Stack:
Inicializační funkce __init__ inicializuje prázdný zásobník. Vrchol
zásobníku bude uzel (hodnota typu Node), je-li zásobník
neprázdný, jinak bude None.
Následuje metoda push. Ta vytvoří nový uzel a nastaví jeho
následníka na stávající vrchol (ať už je to uzel nebo None).
Parametrem metody push je hodnota, kterou chceme do
zásobníku vložit. Uvažme následující situaci před voláním
stack.push(7):
def push(self, item: int) -> None:
Metoda push má pouze tři příkazy. Proto si na ní
detailně ilustrujeme, jak se bude vnitřní struktura
(tvořená zejména atributy next jednotlivých uzlů)
postupně měnit.
Atribut top prozatím obsahuje uzel, který byl doteď
(tzn. těsně před voláním metody push) vrcholem
zásobníku. První příkaz vytvoří nový uzel (voláním
Node(item)) a přiřadí jej do lokální proměnné new.
new = Node(item)
Tento uzel zatím není nijak svázaný se zbytkem seznamu:
V dalším kroku provážeme uzel new se zbytkem seznamu.
Atribut top ovšem stále odkazuje předchozí vrchol
zásobníku.
new.next = self.top
Nová situace:
V posledním krok změníme odkaz (atribut) top tak, aby
ukazoval na nový vrchol.
self.top = new
Atribut top a lokální proměnná new tak sdílí tutéž
hodnotu:
Návratem z metody push lokální proměnná new zanikne, a
atribut top zůstane jediným odkazem na (teď již nový)
vrchol zásobníku. K předchozímu vrcholu se dostaneme skrz
atribut next nového vrcholu:
Druhou metodou je pop, která odstraní prvek (a odpovídající
uzel) ze zásobníku. V obecném případě můžeme samozřejmě metodu
pop volat v libovolném stavu zásobníku. Pro ilustraci ale
předpokládejme, že byla zavolána těsně po ukončení výše
vyobrazeného push(7).
def pop(self) -> int | None:
Nejprve vyřešíme případ, kdy byl zásobník prázdný. To
poznáme tak, že atribut top je nastavený na None.
V takovém případě stav nijak neměníme, a pouze vrátíme
None, čím indikujeme volajícímu, že nebylo ze zásobníku
co odstranit.
if self.top is None:
return None
Na tomto místě již víme, že zásobník je neprázdný, a tedy
atribut top obsahuje nějaký vrchol. Nejprve si poznačíme
hodnotu, která je v tomto uzlu uložena:
result = self.top.item
Po vykonání tohoto příkazu bude lokální proměnná result
sdílet hodnotu s atributem top.item:
Dále přesměrujeme atribut top na nový vrchol. Uvědomte
si, že je-li stav zásobníku X, po provedení dvojice
operací push a pop se tento vrátí do stejného stavu X.
Zejména bude mít tentýž vrchol jako před provedením obou
operací.
self.top = self.top.next
Srovnejte následující situaci se situací vyobrazenou před
voláním push výše.
Všimněte si také, že na původní vrchol zásobníku již
neexistuje žádný odkaz (není uložen v žádné proměnné ani
atributu). V jazyce Python taková hodnota automaticky
zanikne. Zbývá už jen vrátit požadovanou hodnotu:
return result
Po provedení dvojice volání push a pop se tedy dostaneme
do původního stavu. Ještě jednou zdůrazňujeme, že volání
push a pop nemusí být takto provázána vždy. Lze třeba
volat vícekrát za sebou push, nebo pop. Na vyobrazených
situacích to ve skutečnosti nic nemění, s výjimkou konkrétních
čísel uložených v zásobníku.
V této ukázce budeme implementovat (tentokrát neomezenou) frontu
pomocí zřetězeného seznamu. Třída Node bude sloužit jako jeden
uzel fronty:
class Node:
def __init__(self, value: int) -> None:
self.value = value
self.next: Node | None = None
Třída Queue bude implementovat běžné rozhraní fronty (push,
pop) a data bude ukládat do jednoho spojitého řetězu uzlů
(instancí třídy Node).
Hlavu tohoto řetězu (tzn. takový uzel, z kterého lze dojít do
všech ostatních uzlů) uložíme do atributu chain. Řetěz bude mít
právě tolik prvků, kolik jich je uloženo ve frontě a bude ukončen
uzlem, který má next nastavený na None. Výjimku tvoří případ,
kdy je fronta prázdná, kdy není hodnota chain vůbec určena.
def pop(self) -> int | None:
if self.chain is None:
return None
value = self.chain.value
self.chain = self.chain.next
if self.chain is None:
self.insert = None
return value
Všimněte si, že správně implementovaná fronta při žádné operaci
neprochází zřetězený seznam, kterým je reprezentovaná.
V přiložených testech si demonstrujeme zejména to, že fronta bude
funkční i v situaci, kdy ji uměle uprostřed „rozpojíme“ –
samozřejmě jen do chvíle, než by se takové rozpojení dostalo do
hlavy fronty.
Třída Warrior reprezentuje válečníka, který má jméno a sílu.
Tyto jeho vlastnosti bude třída reprezentovat atributy name a
strength. Tato třída obsahuje pouze inicializační funkci __init__.
class Warrior:
def __init__(self, name: str, strength: int) -> None:
self.name = name
self.strength = strength
Velké množství válečníků tvoří hordu, kterou reprezentujeme třídou
Horde. Horda má interní strukturu – je rozdělena do
pojmenovaných klanů, které reprezentujeme slovníkem (jméno klanu,
seznam válečníků).
Metoda (a zároveň predikát) zkontroluje, má-li každý klan
dostatečnou sílu, která je rovna součtu sil všech jeho válečníků.
Měl by vám stačit nanejvýš jeden průchod seznamy válečníků.
Metoda insert vloží do seznamu nový prvek. Nezapomeňte, že
seznam musí být vždy seřazený. Metoda by měla projít celý seznam
nejvíce jednou.
def insert(self, value: int) -> None:
pass
Následující metoda vrátí největší prvek seznamu, jehož hodnoty
spadají do oboustranně uzavřeného intervalu [value, value + dist].
Pokud žádný takový prvek není, vrátí None.
V případech, kdy se tomu lze vyhnout, neprocházejte seznam zbytečně celý.
Naprogramujte třídu TimeInterval, která bude reprezentovat
časový interval. Vstupní podmínkou inicializační funkce je, že
všechny parametry jsou nezáporná čísla a minuty a sekundy jsou
nejvýše 59.
Metoda vrátí reprezentovaný interval jako n-tici ve formátu
(hodiny, minuty, sekundy), kde minuty a sekundy nabývají
hodnoty z uzavřeného intervalu [0, 59].
V této úloze budete programovat třídu Tortoise, která se chová
podobně jako želva, kterou jsme používali v kapitole B. Rozdílem
bude, že naše želva nebude kreslit na obrazovku, ale pouze počítat
své aktuální souřadnice. Souřadnice želvy jsou po každém kroku
celočíselné, ale výpočty provádějte na hodnotách typu float,
které po každém kroku zaokrouhlíte zabudovanou funkcí round.
Všechny kreslící metody želvy budou vracet odkaz na vlastní
instanci, aby bylo lze volání pohodlně řetězit (viz použití
v testech).
Point = tuple[int, int]
class Tortoise:
Želva je po vytvoření otočena v kladném směru osy t.j. „na
sever“ a nachází se v bodě initial_point.
Napište čistou funkci filter_linked, která vytvoří nový
zřetězený seznam, který vznikne z toho vstupního (num_list)
vynecháním všech uzlů s hodnotou menší než lower_bound. Měl by
Vám stačit jeden průchod vstupním seznamem.
V tomto příkladu je zakázáno použití Pythonovských datových struktur
seznam, množina, slovník.
Naprogramujte třídu RingBuffer která se bude chovat jako fronta,
ale bude mít shora omezenou velikost. Pro ukládání dat bude
využívat jinou třídu, SimpleList (tuto třídu nesmíte měnit, ani
přistupovat k jejím atributům), která poskytuje toto rozhraní
(sl je instance SimpleList):
sl.append(x) vloží na konec seznamu prvek x,
sl.get(i) vrátí hodnotu na indexu i,
sl.size() vrátí aktuální velikost seznamu,
sl.set(i, x) nastaví index i na hodnotu x.
Pozor: V žádné metodě neprocházejte celý seznam.
class RingBuffer:
Při inicializaci se nastaví velikost kruhové fronty na size.
Pro ukládání dat bude použita instance třídy SimpleList
předaná parametrem storage.
Metoda push se pokusí přidat prvek na konec fronty. Je-li
fronta plná, metoda vrátí False a nic neudělá. V opačném
případě prvek vloží na konec fronty a vrátí True.
def push(self, value: int) -> bool:
pass
Metoda pop odstraní prvek ze začátku fronty a vrátí jej.
Je-li fronta prázdná, metoda nic neudělá a vrátí None.
Hashovací tabulka je datová struktura, která umožňuje rychlé
ukládání a vyhledávání hodnot. Základem je hashovací funkce, která
určí přihrádku, do níž hodnota patří. V každé přihrádce je pak
jednosměrně zřetězený seznam obsahující hodnoty v dané přihrádce.
V našem příkladu budeme používat hashovací funkci modulo,
konkrétní hodnota modulu bude stanovena při vytváření hashovací
tabulky jako parametr inicializační funkce.
Vaším úkolem bude implementovat třídu HashTable:
Inicializační funkce __init__ vytvoří seznam (typ list)
o m přihrádkách. Každá přihrádka je na začátku tvořená
prázdným zřetězeným seznamem.
Metodu insert, která vloží hodnotu do správné přihrádky.
Vstupní podmínkou je, že hodnota v tabulce není přítomna. Tuto
metodu implementujte co nejefektivněji.
Metodu contains, která zjistí, zda se daná hodnota v tabulce
vyskytuje, či nikoliv.
Metodu remove, která zadanou hodnotu z tabulky odebere.
Metodu bucket, která pro zadaný klíč vrátí hlavu zřetězeného
seznamu, který tvoří klíči příslušnou přihrádku (bez ohledu na
přítomnost klíče v tabulce), nebo None je-li tato prázdná.
Třídu Node nijak neměňte. Tabulka musí fungovat i v případě, že
je seznam vrácený metodou bucket nějak upraven.
† V této úloze budeme programovat dvojitě zřetězený seznam, který se
podobá jednoduše zřetězenému seznamu, který již dobře znáte. Jak
napovídá už název, každý uzel bude připojen do řetězu na obě
strany, tzn. krom následovníka si bude pamatovat i svého
předchůdce.
Oproti seznamu zřetězenému jednoduše se v tom dvojitém lépe
odebírají prvky: z libovolného místa seznamu (tedy zejména na obou
koncích) lze totiž odebrat prvek bez toho, abychom museli seznam
jakkoliv procházet. A proto i Vaše implementace uvedených metod
(kromě search) by měla fungovat bez jakéhokoliv procházení seznamu.
V této úloze naprogramujeme lehce modifikovaný jednosměrně
zřetězený seznam (ten standardní znáte z přednášky a z řešeného
příkladu sorted_list.py). Rozdíl bude spočívat v tom, že
poslední odkaz v seznamu nebude None jako dříve, ale bude
ukazovat na hlavu, čím seznam uzavře do kruhu. Třída Node
reprezentuje jeden uzel. Zvažte, jakého typu by měl být její
atribut next.
class Node:
def __init__(self, value: int) -> None:
self.next = None
self.value = value
Následuje třída CircularList, která má jediný povinný atribut,
head, který ukazuje na hlavu seznamu. V prázdném seznamu by měla
být v head uložena hodnota None. Hned po vytvoření
reprezentuje instance třídy CircularList právě prázdný seznam.
Naznačené metody nechť se chovají následovně:
insert vloží novou hodnotu na začátek seznamu
last vrátí poslední uzel (nikoliv hodnotu)
Tyto metody nepotřebují nijak procházet seznam hodnot.
Metody split_by_value a split_by_node rozdělí stávající seznam
na dva kratší seznamy, a to tak, že uzly od hlavy až k uzlu
popsaného parametrem (včetně) ponechá ve stávajícím seznamu, a ze
zbytku vytvoří nový seznam, který vrátí. Pořadí uzlů (a tedy i
hodnot) musí zůstat zachováno. Metoda split_by_value seznam
rozdělí na prvním výskytu zadané hodnoty. Vstupní podmínky:
hodnota předaná metodě split_by_value musí být v seznamu
aspoň jednou přítomna,
uzel předaný metodě split_by_node patří tomuto seznamu.
Příklad: uvažme hodnotu lst typu CircularList, která obsahuje
prvky 4, 5, 1, 2, 3 a 7. Po provedení příkazu new = lst.split(5)
zbudou v seznamu lst pouze hodnoty 4 a 5, zatímco seznam new
bude mít prvky 1, 2, 3 a 7.
Na vstupu dostanete (standardní Pythonovský) seznam čísel
z rozsahu takový, že každé číslo se v něm vyskytuje
právě jednou, a který tedy popisuje permutaci. Na každém indexu
tohoto seznamu najdete číslo, na které se má daný index permutací
zobrazit. Vaším úkolem je ve funkci shuffle tuto permutaci
aplikovat na vstupní zřetězený seznam (t.j. upravit odpovídajícím
způsobem pořadí jeho uzlů). Předpokládejte, že má právě uzlů.
Nevytvářejte při řešení nové uzly ani nemodifikujte hodnoty (atribut
value) těch existujících. Funkce rovněž nesmí modifikovat vstupní
Pythonovský seznam permutation.
Příklad:
Je-li zadaná permutace , přesune se prvek z pozice 0 na
pozici 2, z pozice 1 na pozici 0 a ten z pozice 2 na pozici 1:
Zadané třídy nijak nemodifikujte.
Zamyslete se nad tím, jak to udělat efektivně. Pro správné
řešení vám postačují dva přechody vstupním zřetězeným seznamem.
class Node:
def __init__(self, value: int) -> None:
self.value = value
self.next: Node | None = None
Metoda books vrátí seznam knih v pořadí, v jakém byly do
knihovny přidány.
def books(self) -> list[Book]:
pass
Metoda group_by_author vrátí slovník, který přiřadí každému
autorovi seznam knih, které napsal. K implementaci této metody
Vám stačí jeden průchod seznamem knih.
Napište čistou funkci, která sestaví zřetězený seznam, který bude
obsahovat hodnoty, které se nachází ve vstupním seznamu na
zadaných indexech. Pořadí hodnot zachovejte. Předpokládejte, že
indexy v seznamu indices jsou platné a vzestupně seřazené.
K implementaci této funkce Vám stačí jeden průchod seznamy
indices a linked.
† Naprogramujte datovou strukturu ‘zipper’: jedná se o strukturu
podobnou zřetězenému seznamu, s jedním důležitým rozdílem: přesto,
že používá jednoduché zřetězení (nikoliv dvojité), lze se v něm
efektivně pohybovat oběma směry. Nicméně na rozdíl od dvojitě
zřetězeného seznamu nám zipper umožňuje udržovat pouze jediný
kurzor.
Jak zipper funguje? Používá následující strukturu:
Jak efektivně kurzor posunout o jednu pozici doleva nebo doprava
si pravděpodobně dovedete představit. Pro jednoduchost budeme
uvažovat pouze neprázdný zipper.
Pro zajímavost: zipper lze implementovat také pomocí dvojice
zásobníků, a tato implementace je typicky efektivnější. V tomto
cvičení ale preferujeme použití zřetězených struktur.
V tomto příkladu je zakázáno použití Pythonovských datových struktur
seznam, množina, slovník.
class Zipper:
def __init__(self, num: int) -> None:
pass
Vrátí aktuální hodnotu kurzoru.
def cursor(self) -> int:
pass
Vloží prvek nalevo od kurzoru.
def insert_left(self, num: int) -> None:
pass
Smaže prvek nalevo od kurzoru, existuje-li takový, a vrátí
jeho hodnotu. Jinak vrátí None.
def delete_left(self) -> int | None:
pass
Posune kurzor o jednu pozici doleva. Není-li se kam posunout,
metoda neudělá nic.
def shift_left(self) -> None:
pass
Posune kurzor o jednu pozici doprava. Není-li se kam posunout,
metoda opět neudělá nic.
† Polynomy jste již potkali v příkladu r4_poly z páté kapitoly.
Připomeňme si, že polynom je výraz tvaru:
Tentokrát budeme polynomy sčítat, odečítat a násobit. Polynom si
pro účely tohoto příkladu zavedeme jako datovou strukturu
s operacemi popsanými níže. Polynomy se sčítají a násobí dle
běžných pravidel – součet se do výsledného polynomu
promítne jako , zatímco výraz povede na
člen . Nezapomeňte, že při násobení dvou polynomů lze
stejnou mocninu dostat různými způsoby, třeba je totéž
jako . Potřebné algoritmy pro výpočet koeficientů
výsledného polynomu si jistě již zvládnete z uvedeného odvodit.
class Polynomial:
Vytvoří nový polynom. Koeficienty ve vstupním seznamu jsou
uloženy v pořadí a tento seznam smí
obsahovat vedoucí nuly. Vnitřní reprezentaci si ovšem můžete
zvolit libovolnou.
Tato kapitola přináší pouze dva nové prvky (oba souvisí s řazením).
Zabudovanou čistou funkci sorted(x), které výsledkem je nový
seznam, který je vzestupně uspořádaný (pro l = sorted(x) a i
<= j platí l[i] <= l[j]), a zároveň obsahuje stejné prvky
jako x. Parametr x může být:
seznam (list),
množina (set),
d.items(), d.keys() nebo d.values() je-li d hodnota
typu slovník (dict).
Zabudovanou metodu-proceduru l.sort(), která přeuspořádá
seznam l tak, aby byl vzestupně seřazený (samotné prvky se při
tom opět nijak nemění).
V této ukázce se budeme zabývat dvěma velmi jednoduchými řadicími
algoritmy založenými na počítání.
První algoritmus funguje pro seznamy, ve kterých se žádná hodnota
neopakuje. Pracuje na velmi jednoduchém principu:
uvažme libovolný prvek vstupního seznamu,
spočítejme kolik se v seznamu nachází celkem prvků, které jsou
menší než ; tuto hodnotu označme ,
v seřazeném seznamu se musí objevit na indexu : index
(je-li počítán od nuly) je právě počet prvků, které dané
hodnotě v seznamu předchází.
Spočítáme-li tedy hodnotu pro každý vstupní prvek, můžeme již
přímočaře sestavit výstupní seznam: ke každému prvku známe index,
na který ho chceme uložit. Čistá funkce count_sort tuto myšlenku
realizuje:
def count_sort(records: list[int]) -> list[int]:
Protože budeme často iterovat sekvencí indexů seznamu
records, uložíme si tuto sekvenci do pomocné proměnné.
indices = [i for i in range(len(records))]
Dále si nachystáme dva seznamy: v jednom budeme počítat
hodnoty , do toho druhého potom vstupní prvky uložíme
vzestupně seřazené.
counts = [0 for _ in indices]
result = [0 for _ in indices]
Hlavní cyklus vypočte do seznamu counts jednotlivé hodnoty
. Nejjednodušeji získáme tak, že spočítáme všechna
taková, že platí .
Abychom si ale ušetřili práci, uvědomíme si, že není potřeba
nejprve při výpočtu vyhodnotit a později při
výpočtu vyhodnotit .
Protože beztak předpokládáme, že se prvky neopakují, platí pro
právě jedna z těchto dvou možností. Platí-li tedy , můžeme srovnání započítat do (našli jsme prvek
menší než ) a naopak, platí-li , srovnání rovnou
započteme do .
for i in indices:
for j in range(i):
if records[j] < records[i]:
counts[i] += 1
else:
counts[j] += 1
Zbývá tedy už jen sestavit výsledný seznam. Připomínáme, že
hodnota je v programu k dispozici jako records[i] a
odpovídající hodnotu máme uloženou v counts[i].
for i in indices:
result[counts[i]] = records[i]
Protože hodnoty se na vstupu neopakují, je v counts uložena
permutace indexů seznamu records: máme tedy zaručeno, že
zapíšeme na každý index seznamu result, a zároveň, že žádnou
hodnotu ze seznamu records neztratíme (nepřepíšeme). To, že
výsledný seznam result bude vzestupně seřazený, je pak již
zřejmé z předchozího.
return result
Druhý algoritmus je v jistém smyslu „opačný“ než ten první: bude
pracovat se seznamy, které obsahují pouze hodnoty z předem daného,
nepříliš velkého rozsahu . Protože hodnot je málo, budou
se v delších seznamech často opakovat. Algoritmus je také velmi
jednoduchý:
pro každou hodnotu z rozsahu spočítáme, kolikrát
se ve vstupním seznamu nachází; tento počet označíme kde
,
s použitím této informace sestavíme výsledný seznam tak, že
pro každou hodnotu do něj vložíme kopií
hodnoty (zde opět ).
Tento algoritmus je realizován čistou funkcí distribution_sort:
Sekvenci všech hodnot, které se na vstupu mohou objevit si, ve
vzestupném pořadí, uložíme do proměnné values. Zároveň si
nachystáme seznam counts, ve kterém budeme počítat hodnoty
.
values = [i for i in range(low, high)]
counts = [0 for _ in values]
Nyní zjistíme počet výskytů každé hodnoty z values ve
vstupním seznamu records:
for record in records:
counts[record - low] += 1
A sestavíme výsledný seznam.
result = []
for value in values:
for _ in range(counts[value - low]):
result.append(value)
return result
Přestože řadicí algoritmy, které jsme implementovali, jsou velmi
jednoduché, není těžké v nich udělat chybu. A to navíc třeba
takovou, že se bude projevovat jen vzácně. Proto tyto algoritmy
otestujeme obzvlášť důkladně. Funkce test_parameters definovaná
níže popisuje parametry seznamů, které budeme testovat: rozsah
hodnot (hodnoty budou spadat do rozsahu low <= value < high) a
počet prvků. Pro danou sadu parametrů vygenerujeme všechny možné
seznamy tak, aby splnily vstupní podmínky (v případě funkce
count_sort se hodnoty nesmí opakovat) a ověříme dvě definující
vlastnosti řazení:
výstup je permutací vstupu,
výstup je seřazený.
def main() -> None: # demo
for low, high, count in test_parameters():
for records in all_lists(low, high, count, False, []):
result = count_sort(records)
assert is_permutation(result, records)
assert is_sorted(result)
for low, high, count in test_parameters():
for records in all_lists(low, high, count, True, []):
result = distribution_sort(records, low, high)
assert is_permutation(result, records)
assert is_sorted(result)
def is_permutation(a: list[int], b: list[int]) -> bool:
result = [0 for _ in range(max(a + b) + 1)]
for item in a:
result[item] += 1
for item in b:
result[item] -= 1
for diff in result:
if diff != 0:
return False
return True
def is_sorted(records: list[int]) -> bool:
for i in range(len(records) - 1):
if records[i] > records[i + 1]:
return False
return True
result = []
for x in range(low, high):
if repeats or x not in prefix:
result.extend(all_lists(low, high, count - 1, repeats,
prefix + [x]))
return result
def test_parameters() -> list[tuple[int, int, int]]:
result = []
for high in range(10):
for low in range(high):
for count in range(1, 5):
result.append((low, high, count))
return result
V této ukázce si ukážeme další řadicí algoritmus, tentokrát budeme
ale řadit zřetězené seznamy, které nelze efektivně indexovat.
Jejich výhodou je naopak možnost levně vkládat hodnoty doprostřed:
proto si na nich demonstrujeme tzv. insertion sort, neboli
řazení vkládáním. Myšlenka tohoto algoritmu je také velmi
jednoduchá:
vytvoříme prázdný výstupní seznam,
prvky postupně odebíráme ze začátku vstupního seznamu,
pro každý odebraný vstupní prvek najdeme ve vznikajícím
výstupním seznamu správné místo a tam ho vložíme.
Nejprve si definujeme složený datový typ, kterým budeme
reprezentovat zřetězené seznamy:
class Node:
def __init__(self, value: int):
self.next: Node | None = None
self.value = value
Nyní již můžeme přistoupit k samotnému zápisu algoritmu pro řazení
vkládáním. Oproti předchozí ukázce bude algoritmus realizovat
procedura. Nazveme ji insert_sort, a bude přesně kopírovat
postup z úvodního odstavce. Abychom zachovali jednoduchou a jasnou
strukturu hlavního výpočtu, veškeré pomocné výpočty oddělíme do
pomocných procedur.
def insert_sort(records: LinkedList) -> None:
out = LinkedList()
while records.head is not None:
to_insert = remove_head(records)
insert_sorted(out, to_insert)
V seznamu out máme nyní seřazený výsledek, naším úkolem ale
bylo přeuspořádat stávající seznam records, který je nyní
prázdný. Proto do něj „převěsíme“ celý seznam out.
records.head = out.head
První pomocná procedura, remove_head, oddělí hlavu neprázdného
seznamu, a vrátí ji jako samostatný (izolovaný) uzel.
def remove_head(lst: LinkedList) -> Node:
assert lst.head is not None
result = lst.head
lst.head = lst.head.next
result.next = None
return result
Další pomocná procedura, insert_sorted, vloží uzel do
seřazeného seznamu, a to tak, že výsledný seznam zůstane seřazený
(jeho délka se přitom zvýší o jedna). Více explicitně, procedura
insert_sorted má tyto vstupní podmínky:
out je seřazený zřetězený seznam (může být prázdný),
node je uzel, který není součástí out.
Výstupní podmínkou je:
out je seřazený seznam a node je součástí out.
Samotné vložení uzlu je jednoduché: „těžká“ část této procedury je
nalézt vhodné místo, kam uzel vložit. Tuto část oddělíme do
pomocné čisté funkce, find_position, která vrátí dvojici uzlů,
mezi které budeme uzel vkládat. Jeden, nebo i oba vrácené uzly
mohou být None.
def insert_sorted(out: LinkedList, node: Node) -> None:
before, after = find_position(out, node.value)
if before is None:
out.head = node
else:
before.next = node
node.next = after
Zbývá nám definovat poslední, a v podstatě i nejsložitější,
podprogram. Na rozdíl od těch předchozích se bude jednat o čistou
funkci: vstupní seznam nebudeme nijak měnit. Tato funkce má
následující vstupní podmínku:
items je seřazený zřetězený seznam,
value může ale nemusí být v seznamu přítomna.
Nazveme-li složky návratové hodnoty before a after, výstupní
podmínku můžeme popsat takto:
before i after jsou None a items je prázdný, nebo
before je None a value <= after.value, nebo
after je None a before.value <= value, nebo
before.value <= value <= after.value.
def find_position(items: LinkedList, value: int) \
-> tuple[Node | None, Node | None]:
before = None
after = items.head
while after is not None and value >= after.value:
before = after
after = after.next
return (before, after)
Tím je definice procedury insert_sort a jejích pomocných
podprogramů hotova. Zbývá nám proceduru otestovat: na to budeme
potřebovat další dvě pomocné funkce (obě budou čisté):
to_linked_list která z klasického Pythonovského seznamu vytvoří
seznam zřetězený, a funkce to_python_list která provede konverzi
opačnou.
def to_linked_list(items: list[int]) -> LinkedList:
out = LinkedList()
for i in range(len(items) - 1, -1, -1):
node = Node(items[i])
node.next = out.head
out.head = node
return out
def to_python_list(items: LinkedList) -> list[int]:
ptr = items.head
out = []
while ptr is not None:
out.append(ptr.value)
ptr = ptr.next
return out
Stejně jako v předchozí ukázce budeme proceduru insert_sort
testovat pro všechny seznamy z parametrické rodiny. Přípustné
kombinace parametrů nám bude generovat funkce test_parameters,
jako seznam trojic: nejmenší a největší číslo, které se objeví, a
celková délka seznamu.
def test_parameters() -> list[tuple[int, int, int]]:
result = []
for high in range(10):
for low in range(high):
for count in range(1, 5):
result.append((low, high, count))
return result
Funkce main podle parametrů z test_parameters vygeneruje
všechny odpovídající seřazené seznamy, a pro každý seřazený
seznam ověří, že procedura insert_sort korektně seřadí všechny
jeho permutace.
def main() -> None: # demo
for low, high, count in test_parameters():
for records in all_lists(low, high, count, True, []):
linked = to_linked_list(records)
insert_sort(linked)
result = to_python_list(linked)
assert is_sorted(result)
assert is_permutation(result, records)
V poslední ukázce pro tento týden se budeme zabývat hledáním
v seřazeném seznamu. V krátkých seznamech si můžeme dovolit hledat
„naivně“: srovnáme hledanou hodnotu postupně s každým prvkem. Je
zřejmé, že v nejhorším případě musíme provést tolik srovnání,
kolik prvků je v prohledávaném seznamu.
Je-li ale seznam seřazený, můžeme hledání velmi výrazně urychlit.
Technika, kterou k tomu použijeme se jmenuje půlení intervalu.
Ač to nemusí být na první pohled zřejmé, je velmi důležité, abyste
princip této techniky pochopili, protože na ní staví řada
fundamentálních výsledků, které budete v dalších semestrech
studovat.
Základní myšlenkou algoritmu je rozdělit si vstupní seznam na dvě
přibližně stejně dlouhé poloviny. Je-li hodnota v seznamu
přítomna, musí se nacházet v jedné z těchto dvou částí. Protože
celý seznam je seřazený, platí to i o každém jeho podseznamu,
zejména to tedy platí o našich přibližných polovinách.
Je-li nějaký hodnota value přítomná v seznamu list, musí nutně
platit min(list) <= value <= max(list). Je-li list navíc
vzestupně seřazený, platí min(list) == list[0] a max(list) ==
list[-1]. Celkem tedy list[0] <= value <= list[-1].
Protože se jedná o podmínku nutnou, není-li splněna, můžeme
s jistotou říci, že se hledaná hodnota v daném (pod)seznamu
nenachází. Zjistíme-li tedy, že tuto nutnou podmínku některý
z našich podseznamů porušuje, nemusíme se tímto nadále vůbec
zabývat: stačí nám vyřešit problém pouze pro zbývající polovinu.
Zbývá nám vyřešit konkrétní zápis této myšlenky. Zejména se
chceme vyhnout vytváření nových seznamů: tato operace je
drahá, a ve výsledku bychom pak oproti naivnímu hledání nic
neušetřili. Můžeme si ale pamatovat rozsah indexů ve kterém
aktuálně hledáme. Indexy si nazveme (low) a
(high), a budeme je chápat jako polouzavřený interval : index (low) do rozsahu patří, index (high) už
nikoliv. Zejména to znamená, že interval je prázdný právě
když low == high.
Na začátku výpočtu prohledáváme celý seznam, proměnné low a
high tedy nastavíme na příslušné hodnoty:
low, high = 0, len(records)
Hledání pokračuje dokud je prohledávaný (pod)seznam neprázdný.
Najdeme-li hledanou hodnotu, cyklus ukončíme dříve: skončí-li
tedy cyklus pro nesplnění podmínky, hledaná hodnota v seznamu
nebyla přítomna (za předpokladu, že hodnota v seznamu byla
přítomna, musí být přítomna v prázdném seznamu → spor).
while low < high:
Stávající seznam si rozdělíme na ony avizované „přibližně
stejně velké“ části (jejich délka se může lišit
o jedničku, byl-li seznam liché délky). Dělení provedeme
na indexu (mid). První podseznam je tedy .
Ten druhý by pak měl být , nicméně je praktičtější
použít .
Proč jsme vypustili samotné (mid)? Jedná se o právě
jeden prvek, se kterým se bude tedy dobře pracovat
(nemusíme si hlídat existenci). Navíc nám jeho vyloučení
z dalšího hledání zaručuje, že se prohledávaný seznam
v každé iteraci zkrátí aspoň o jedničku. Nehrozí nám tak,
že se program „zacyklí“ na nějakém okrajovém případu,
který jsme neošetřili.
mid = low + (high - low) // 2
Jako první ověříme, zda na indexu není uložena hledaná
hodnota: pokud ano, hledání ukončíme. V opačném případě
víme, že index můžeme z dalších úvah vypustit.
Navíc musíme zdůvodnit, proč musí nutně index
v některé iteraci ukazovat na hledanou hodnotu, byla-li
v seznamu přítomna. Uvědomme si, že struktura algoritmu je
taková, že je-li prvek přítomen, je nutně přítomen
v rozsahu . Zároveň se v každé iteraci interval
striktně zmenšuje, a vždy leží v tomto intervalu.
Konečně nejmenší neprázdný interval vede na , jediný prvek v tomto intervalu je tudíž na indexu ,
a hledanou hodnotu tedy zaručeně najdeme nejpozději ve
chvíli, kdy .
if records[mid] == value:
return True
Dále tedy zkontrolujeme podseznam : je-li value
v této části seznamu, platí již zmiňovaná nutná podmínka:
records[low] <= value <= records[mid], zejména pak její
druhá část: value <= records[mid].
Tuto znegujeme na records[mid] < value: platí-li tato
negace, nutná podmínka je porušena a hodnota value se
v této části seznamu nenachází. Proto prohledávaný
interval zúžíme na a pokračujeme další
iterací.
if records[mid] < value:
low = mid + 1
Zbývá provést analogickou kontrolu pro rozsah .
Můžeme-li přítomnost value v této části vyloučit, budeme
se v další iteraci zabývat už pouze podseznamem .
if records[mid] > value:
high = mid
Jak již bylo zmíněno dříve, dojde-li k ukončení cyklu proto,
že nám k prohledání zbyl prázdný podseznam, víme, že hledaný
prvek v seznamu nebyl přítomen. Vrátíme tedy False.
return False
Tím je implementace hotova. Podobně jako u řadicích algoritmů
budeme hledání půlením intervalu testovat velmi pečlivě: nejprve
vygenerujeme každý seřazený seznam v daném rozsahu parametrů. Pro
každý z nich pak ověříme, že výsledek hledání je správný, a to jak
pro hodnoty, které jsou v seznamu přítomny, tak i hodnoty, které
v něm nejsou (buď chybí, nebo jsou mimo rozsah hodnot).
def main() -> None: # demo
for low, high, count in test_parameters():
for records in sorted_lists(low, high, count, []):
for v in range(low - 1, high + 1):
assert bin_search(records, v) == (v in records)
result = []
for x in range(low, high):
result.extend(sorted_lists(x, high, count - 1, prefix + [x]))
return result
def test_parameters() -> list[tuple[int, int, int]]:
result = []
for high in range(10):
for low in range(high):
for count in range(0, 8):
result.append((low, high, count))
return result
Napište čistou funkci, která najde v zadaném uspořádaném seznamu
numbers největší číslo, které není větší než parametr value.
Neexistuje-li takové, vraťte None.
V ostatních případech je tedy výsledkem vždy číslo, které se
nachází v numbers a vždy platí lower_bound(numbers, x) ≤ x.
Předpokládejte, že v seznamu numbers se čísla neopakují.
Očekávaná složitost řešení je logaritmická.
def lower_bound(numbers: list[int], value: int) -> int | None:
pass
Implementujte čistou funkci count_in_sorted, která ve vzestupně
seřazeném seznamu records co nejefektivněji spočte počet výskytů
hodnoty value. K hodnotám v records přistupujte použitím
metody get: např. records.get(7) vrátí hodnotu na indexu 7.
Délku seznamu získáte voláním records.size().
Dobré řešení úlohy je logaritmické časové složitosti.
Napište čistou funkci local_extremes, která dostane na vstupu
seznam values čísel a vrátí dvojici seznamů min_indices,
max_indices. Každý prvek seznamu values je unikátní. Seznam
min_indices (max_indices) bude obsahovat indexy lokálních
minim (maxim) seznamu values. Oba tyto seznamy budou vzestupně
seřazené. Řešení očekáváme v lineární časové složitosti.
Implementujte predikát is_cyclically_sorted, který je pravdivý,
je-li seznam cyklicky seřazený. Seznam je cyklicky seřazený,
existuje-li rotace, po které bude seřazený vzestupně.
Měli byste být schopni napsat řešení, jehož složitost je lineární.
Implementujte čistou funkci frequency_sort, která podle
frekvencí výskytu seřadí hodnoty v seznamu values. Hodnoty se
stejnou frekvencí výskytu nechť jsou seřazeny vzestupně podle
hodnoty samotné. Výsledný seznam bude obsahovat každou hodnotu
právě jednou.
† Třída LinkedList reprezentuje zřetězený seznam, se kterým
budete pracovat. Uzly tohoto seznamu mají atribut next, ke
kterému můžete libovolně přistupovat a měnit ho a metodu
compare, která srovná hodnoty uložené ve dvou uzlech. K samotným
hodnotám přímo přistupovat nesmíte. Volání a.compare(b) vrátí
(-1, 0, 1) je-li hodnota v uzlu a (menší, stejná, větší) než
hodnota v uzlu b. První uzel je uložen v atributu head.
Třídy LinkedList a Node nijak nemodifikujte.
Napište funkci merge, která spojí 2 vzestupně seřazené zřetězené
seznamy do jediného seřazeného seznamu. Funkce nevytváří nové
uzly, pouze přepojuje ukazatele next stávajících uzlů z obou
seznamů. Seznamy lze spojit v lineárním čase.
† Implementujte co nejefektivněji čistou funkci unique, která
vrátí seznam unikátních prvků ze vzestupně seřazeného seznamu
values. Vstupní seznam je reprezentován třídou, která poskytuje
pouze metody get(i) (vrátí i-tý prvek) a size (vrátí počet
prvků). Výsledný seznam je běžný seznam typu list a bude také
vzestupně seřazený. Funkci je možné napsat efektivněji než s lineární
složitostí.
Implementujte funkci left_bound, která ve vzestupně seřazeném
seznamu values co nejefektivněji najde index prvního výskytu
hodnoty target. Pokud se hodnota v seznamu nenachází, vrátí
None. V této úloze je lineární řešení neefektivní.
def left_bound(values: list[int], target: int) -> int | None:
pass
Implementujte čistou funkci sort_nested, která vzestupně
uspořádá prvky v seznamu seznamů čísel lists, a to tak, že
přeuspořádá jenom čísla ve vnitřních seznamech, aniž by měnil
jejich délku. Výstupní seznam bude tedy obsahovat stejný počet
stejně dlouhých seznamů jako ten vstupní, ale v obecném případě
budou tyto vnořené seznamy obsahovat jiná čísla.
Implementujte predikát is_almost_sorted, který je pravdivý,
je-li v seznamu items potřeba prohodit právě jednu dvojici
různých čísel, aby se stal vzestupně seřazeným.
Existuje řešení, jehož časová složitost je lineární.
† Napište funkci next_greater, která vrátí nejmenší větší číslo
se stejnými ciframi jaké má číslo number. Pokud větší číslo
neexistuje, funkce vrací None. Nezkoušejte všechny permutace
cifer, existuje efektivnejší řešení.
† Implementujte algoritmus řazení haldou. Základní myšlenka
algoritmu je podobná algoritmu řazení výběrem:
vstupní seznam rozdělíme na dvě pomyslné části, neseřazenou
(na začátku) a seřazenou (na konci);
v každé iteraci najdeme největší prvek v neseřazené části,
přesuneme ho na její konec, a pomyslnou dělící čáru posuneme
o jeden prvek doleva.
To, čím se algoritmus od řazení výběrem liší je metoda „hledání“
onoho největšího prvku. Seznam totiž před samotným začátkem řazení
přeuspořádáme do formy tzv. haldy, která má tyto vlastnosti:
největší prvek je na nulté pozici,
pro prvek na pozici platí, že je větší než prvky na
pozicích a (existují-li).
Je zřejmé, že nahrazením největšího prvku tuto vlastnost můžeme
lehce pokazit. Klíčové pozorování je, že její obnovení je snadné
(a zejména rychlé). Začneme od indexu i = 0 a opakovaně (tak
dlouho, dokud index i ukazuje dovnitř neuspořádané části pole):
vybereme index největšího prvku z možností i, 2*i + 1 nebo
2*i + 2 – pokud jsme vybrali i, jsme hotovi;
v opačném případě vyměníme vybraný prvek s tím na indexu i
a i nastavíme na index vybraný v předchozím kroku.
Mělo by být vidět, že za předpokladu, že před výměnou největšího
prvku měl seznam vlastnosti haldy, uvedenou procedurou je opět
získá (její obvyklý název je sift_down). Zbývá tedy zajistit,
aby mělo vstupní pole tyto vlastnosti i před samotným začátkem
řazení.
Toho dosáhneme například tak, že budeme opakovaně spouštět
proceduru sift_down s počáteční hodnotou i nastavenou postupně
na hodnoty kde je délka vstupního
seznamu. Proč tato procedura funguje se dozvíte například v článku
„Heapsort“ v anglické wikipedii.
def heapsort(records: list[int]) -> None:
pass
def test_parameters() -> list[tuple[int, int, int]]:
result = []
for high in range(10):
for low in range(high):
for count in range(1, 5):
result.append((low, high, count))
return result
† Posledním řadicím algoritmem, který v této kapitole prozkoumáme,
je řazení po číslicích: obvyklé jméno pro tento algoritmus je
„radix sort“, případně „bucket sort“. Algoritmy, které jsme viděli
dosud, pracují všechny (krom distribution_sort) na principu
srovnávání dvojic prvků. Tento princip je velmi obecný, ale často
také omezující.
V této úloze se vrátíme k myšlence funkce distribution_sort a
místo porovnávání prvků je budeme počítat, zvolíme si ale jiné
kritérium. Naším cílem bude seřadit seznam čísel, a využijeme
k tomu skutečnosti, že čísla lze rozložit na jednotlivé cifry
(v nějaké poziční soustavě). Pro jednoduchost si zvolme soustavu
desítkovou (algoritmus ve skutečnosti ale na konkrétní volbě
soustavy nezávisí).
Základním stavebním kamenem bude procedura sort_by_digit, která:
přeuspořádá vstupní seznam tak, aby byl uspořádaný podle
-té číslice,
a to tak, aby přitom nezměnila relativní pořadí prvků, které
mají na -té pozici stejnou číslici.
Protože číslic je málo, ale hodnot v seznamu potenciálně hodně,
hodí se na toto přeuspořádání právě funkce distribution_sort:
spočítáme, kolik vstupních čísel padne do kterého „kyblíčku“
(rozsahu prvků se stejnou -tou cifrou),
pro každý kyblíček spočítáme, na jakých indexech se bude ve
výsledném seznamu nacházet,
vstupní seznam v jednom průchodu do takto nachystaných
kyblíčků rozřadíme (kyblíčky zaplňujeme ve stejném pořadí,
v jakém iterujeme vstupní seznam).
Vyzbrojeni procedurou sort_by_digit už lehce seznam seřadíme:
začneme od poslední cifry, a postupujeme doleva. Lehce se
o správnosti tohoto postupu přesvědčíme indukcí:
po první iteraci je seznam seřazen podle první (nejpravější)
cifry,
předpokládejme, že po -té iteraci je seznam seřazen podle
cifer ; v iteraci bude procedurou
sort_by_digit seřazen podle cifry , ale ta nezměnila
pořadí prvků, které jsou na pozici stejné: proto je
po iteraci seznam seřazen podle cifer .
Následující seznam je již seřazen podle nejnižší cifry. Ukažme si
na něm zbytek algoritmu:
Spočítáme počty cifer na prostřední pozici a dostaneme: 3× 1, 2×
2, 4× 3. Nachystáme si příslušné kyblíčky a vyplňujeme je
(například) zleva doprava:
Postup opakujeme na nejlevější pozici: 4× 1, 1× 2, 4× 3
Implementujte proceduru sort_linked_list, která vzestupně seřadí
zadaný zřetězený seznam. Nevytvářejte přitom žádné nové uzly ani
nemodifikujte hodnoty (atributy value) těch existujících.
Seřazení je třeba provést pouze pomocí změn atributů next (a head).
Není třeba vymýšlet nějaké optimalizace, kvadratické řešení je zde
v pořádku.
V tomto příkladu je zakázáno použití Pythonovských datových struktur
seznam, množina, slovník.
Implementujte proceduru, která dostane na vstup vzestupně seřazený
jednosměrně zřetězený seznam, z tohoto seznamu odstraní všechny duplikáty
(uzly se stejnými hodnotami) tak, že v něm nechá vždy pouze první výskyt.
Odstraněné uzly funkce spojí do nového zřetězeného seznamu (se zachováním
jejich pořadí) a ten vrátí.
Při řešení neměňte hodnoty atributu value ani nevytvářejte nové
uzly typu Node, tj. jediné, co můžete s uzly dělat, je měnit odkazy
na následující uzel.
V tomto příkladu je zakázáno použití Pythonovských datových struktur
seznam, množina, slovník.
Příklad: Je-li zřetězený seznamu tvaru 1 → 2 → 2 → 2 → 7 → 7 → 10,
pak procedura modifikuje tento seznam do tvaru 1 → 2 → 7 → 10 a vrátí
zřetězený seznam tvaru 2 → 2 → 7.
Implementujte proceduru, která dostane na vstup dva vzestupně seřazené
jednosměrně zřetězené seznamy a z prvního z těchto seznamů odstraní uzly
s hodnotami, které se vyskytují ve druhém seznamu.
Druhý zřetězený seznam musí zůstat nezměněn.
Při řešení neměňte hodnoty atributu value ani nevytvářejte nové
uzly typu Node, tj. jediné, co můžete s uzly dělat, je měnit odkazy
na následující uzel.
Očekávané řešení má složitost lineární vůči součtu délek vstupních seznamů.
V tomto příkladu je zakázáno použití Pythonovských datových struktur
seznam, množina, slovník.
Příklad: Je-li první zřetězený seznamu tvaru 1 → 3 → 5 → 5 → 7 → 10
a druhý zřetězený seznam tvaru 1 → 1 → 2 → 5 → 12,
pak procedura upraví první seznam do tvaru 3 → 7 → 10 (a druhý seznam
nechá v původní podobě).
c_life – celulární automat ve stylu „Game of Life“,
d_tetris – hra Tetris.
První úkol vyžaduje pouze základní použití seznamů (z prvního
bloku), další dva úkoly k tomu přidávají datové struktury z páté
kapitoly a poslední úkol navíc využívá uživatelsky definované
datové typy (třídy).
V tomto úkolu se budeme zabývat skladem zboží. Zboží je ve skladu uloženo po
balících, které reprezentujeme trojicemi hodnot: množství (počet jednotek)
zboží, jednotková cena zboží a datum exspirace. Všechny tři hodnoty budou
vždy kladná celá čísla, přičemž datum exspirace bude vždy zadáno tak,
aby jeho zápis v desítkové soustavě byl ve formátu YYYYMMDD dle ISO 8601.
Package = tuple[int, int, int] # amount, price, expiration date
Obsah skladu budeme reprezentovat seznamem balíků, přičemž tento seznam bude
vždy seřazen sestupně dle data exspirace. (Je zájmem společnosti, které sklad
patří, aby se jako první prodaly balíky, jejichž konec trvanlivosti se blíží;
přitom balíky budeme prodávat od konce seznamu.)
Nejprve implementujte funkci remove_expired, která ze skladu odstraní
všechny balíky s prošlou trvanlivostí (tj. ty, jejichž datum exspirace
předchází dnešnímu datu today, které je zadáno stejně jak je popsáno výše).
Funkce vrátí seznam odstraněných balíků v opačném pořadí, než byly umístěny
ve skladu.
Dále pak implementujte funkci try_sell, která uskuteční prodej při zadaném
maximálním množství max_amount a zadané maximální průměrné jednotkové ceně
max_price. Přitom je cílem prodat co nejvíce zboží (v rámci respektování
zadaných limitů). Prodávat je možno jak celé balíky, tak i jen jejich části;
je tedy dovoleno existující balík rozbalit a odebrat z něj jen několik
jednotek zboží (tím vlastně z jednoho balíku vzniknou dva – jeden zůstane ve
skladu, druhý se dostane ke kupci). Je ovšem třeba postupovat tak, že se
balíky odebírají pouze z konce seznamu reprezentujícího sklad – tj. není
možno prodat balík (nebo jeho část), aniž by předtím byly prodány všechny
balíky nacházející se v seznamu za ním. Funkce vrátí seznam balíků, které se
dostaly ke kupci, a to v tom pořadí, jak se postupně ze skladu odebíraly.
Pro příklad uvažujme sklad s následujícími balíky (datum exspirace zde
neuvádíme, horní číslo je množství, spodní cena; pořadí balíků odpovídá
seřazení seznamu, prodáváme tedy „zprava“):
Pokud by přišel požadavek na prodej s maximálním množstvím 500 a maximální
průměrnou jednotkovou cenou 9, pak se prodá pouze celý balík A.
Pokud by místo toho byla požadovaná maximální průměrná cena 12, pak se
prodá celý balík A a 25 jednotek zboží z balíku B.
(Balík B se tedy rozdělí: ve skladu zůstane balík s množstvím 75, ke
kupci se dostane balík s množstvím 25.)
Pokud by byla požadovaná maximální průměrná cena 14, pak se prodá celý
balík A a 70 jednotek zboží z balíku B.
Pokud by byla požadovaná maximální průměrná cena 15, pak se prodají celé
balíky A, B a C.
Pokud by byla požadovaná maximální průměrná cena 16, pak se prodají celé
balíky A, B, C a dvě jednotky zboží z balíku D.
Konečně pro maximální průměrnou cenu 81 se prodají všechny balíky.
Představte si, že máme plán ve tvaru neomezené čtvercové sítě, na níž jsou
položeny čtvercové dílky s nákresy ulic či křižovatek (něco jako kartičky ve
hře Carcassone). Tyto dílky budeme reprezentovat jako množiny směrů, kterými
je možné dílek opustit. Tedy např. dílek {NORTH, SOUTH} je ulice, která
vede severojižním směrem, dílek {EAST, SOUTH, WEST} je křižovatka ve
tvaru T, dílek {EAST} je slepá ulice (z toho dílku je možné se posunout
pouze na východ, ale nikam jinam). Dovolujeme i prázdnou množinu, což je
dílek, z nějž se nedá pohnout nikam.
Heading = int
NORTH, EAST, SOUTH, WEST = 0, 1, 2, 3
Tile = set[Heading]
Situaci na čtvercové síti popisujeme pomocí slovníku, jehož klíči jsou
souřadnice a hodnotami dílky. Na souřadnicích, které ve slovníku nejsou,
se žádný dílek nenachází. Souřadnice jsou ve formátu (x, y), přičemž
x se zvyšuje směrem na východ a y směrem na jih.
Position = tuple[int, int]
Plan = dict[Position, Tile]
Napište nejprve predikát is_correct, který vrátí True právě tehdy, pokud
na sebe všechny položené dílky správně navazují. Tedy je-li možno dílek
nějakým směrem opustit, pak v tomto směru o jednu pozici vedle leží další
dílek, a navíc je z tohoto dílku možné se zase vrátit.
def is_correct(plan: Plan) -> bool:
pass
Dále implementujte čistou funkci run, která bude simulovat pohyb robota
po plánu a vrátí jeho poslední pozici. Předpokládejte přitom, že plán je
korektní (ve smyslu predikátu is_correct výše) a že robotova počáteční
pozice je na některém z položených dílků. Robot se pohybuje podle
následujících pravidel:
Na počáteční pozici si robot vybere první ze směrů, kterým je možné se
pohnout z počátečního dílku, a to v pořadí sever, východ, jih, západ.
Pokud se z počáteční pozice není možné pohnout vůbec, funkce končí.
V dalších krocích robot preferuje setrvat v původním směru (tj. pokud může
jít rovně, půjde rovně). Není-li to možné, pohne se robot jiným ze směrů na
aktuálním dílku – nikdy se ovšem nevrací směrem, kterým přišel (pokud dojde
do slepé ulice, zastaví) a má-li více možností, vybere si tu, která pro něj
znamená otočení doprava.
Pokud robot přijde na dílek, kde už někdy v minulosti byl, zastaví.
Hru Life už jste si možná zkusili implementovat v rámci rozšířených
příkladů ve čtvrté kapitole. V tomto úkolu budete implementovat její trochu
složitější verzi. Místo jednoho života budeme simulovat souboj dvou různých
organismů (modré a oranžové buňky), pozice po úmrtí buňky bude po několik kol
neobyvatelná a budeme mít trochu jiná pravidla pro to, kdy buňky vznikají
a zanikají. Kromě toho bude náš „svět“ neomezený a bude obsahovat „otrávené“
oblasti, kde žádné buňky nepřežijí.
Stav „světa“ je dán slovníkem, jehož klíči jsou 2D souřadnice a hodnotami
čísla od jedné do šesti:
číslo 1 reprezentuje živou modrou buňku,
číslo 2 reprezentuje živou oranžovou buňku,
čísla 3 až 6 reprezentují pozici, kde dříve zemřela buňka
(čím větší číslo, tím víc času od úmrtí buňky uplynulo).
Pozice, které nejsou obsaženy ve slovníku, jsou prázdné.
Position = tuple[int, int]
State = dict[Position, int]
Stejně jako ve hře Life, za okolí pozice považujeme sousední pozice
ve všech osmi směrech, tj. včetně diagonál.
Základní pravidla vývoje světa jsou následující:
Pokud jsou v okolí prázdné pozice přesně tři živé buňky, vznikne zde
v dalším kole buňka nová. Barva nové buňky odpovídá většinové barvě
živých buněk v okolí. Jinak zůstává prázdná pozice prázdnou.
Pokud je v okolí živé buňky tři až pět živých buněk (na barvě nezáleží),
buňka zůstane živou i v dalším kole (a ponechá si svou barvu).
V opačném případě buňka umře a stav této pozice v dalším kole bude číslo 3.
Má-li pozice stav 3 až 5, pak v dalším kole bude mít stav o jedna větší.
Má-li pozice stav 6, v dalším kole bude prázdná.
„Otrávené“ pozice jsou zadány extra (jako množina) a mění základní pravidla
tak, že živé buňky na otrávených pozicích a v jejich okolí vždy zemřou
a na těchto pozicích (otrávených a jejich okolí) nikdy nevzniknou nové buňky.
Napište čistou funkci evolve, která dostane počáteční stav světa initial,
množinu „otrávených“ pozic poison a počet kol generations a vrátí stav
světa po zadaném počtu kol.
Pro vizualizaci je vám k dispozici soubor game_life.py, který vložte do
stejného adresáře, jako je soubor s vaším řešením. Na začátku tohoto souboru
jsou parametry vizualizace (velikost buněk, rychlost vývoje), popis
iniciálního stavu světa a „otrávených“ pozic. Vizualizace volá vaši funkci
evolve s parametrem generations vždy nastaveným na 1.
Jistě už jste někdy slyšeli o hře Tetris. Pokud ne, vítejte v civilizaci!
Hledat můžete začít například tady: https://duckduckgo.com/?q=tetris.
V tomto domácím úkolu si klon této hry naprogramujete.
Abyste si hru mohli vyzkoušet (poté, co implementujete všechny níže
uvedené metody), je vám k dispozici soubor game_tetris.py, který vložte do
stejného adresáře, jako je soubor s vaším řešením, případně jej upravte
dle komentářů na jeho začátku a spusťte.
Hra se ovládá těmito klávesami:
pohyb doleva: šipka doleva nebo A,
pohyb doprava: šipka doprava nebo D,
pohyb dolů: šipka dolů nebo S (děje se také automaticky s nastavenou
prodlevou),
rychlý pád dolů: mezerník,
otočení proti směru hodinových ručiček: Q nebo Page Up,
otočení po směru hodinových ručiček: E nebo Page Down,
ukončení hry: X,
restart: R.
Třída Tetris, kterou máte implementovat, reprezentuje stav hry, tj. obsah
herní oblasti (již spadlé kostky), aktuálně padající blok, jeho pozici
a aktuální skóre. Způsob reprezentace je na vás. Testy i grafické rozhraní
používají ke komunikaci s vaší třídou pouze zde popsané metody.
Rozměry herní oblasti budou zadány při inicializaci (funkci __init__).
Všechny pozice mimo zadané rozměry považujeme za neprostupnou zeď.
Souřadnice zde používáme ve tvaru (sloupec, řádek), přičemž pozice (0, 0)
je v levém horním rohu herní oblasti. Čísla sloupců rostou zleva doprava,
čísla řádků shora dolů.
Padající bloky reprezentujeme seznamem relativních souřadnic, přičemž (0, 0)
je střed otáčení. Tedy např. [(-1, 0), (0, 0), (1, 0), (0, 1)] je tetromino
tvaru T otočené směrem dolů, které se bude otáčet kolem své prostřední
kostky. Blok [(-1, -1), (0, -1), (1, -1), (0, 0)] má stejný tvar, ale otáčí
se kolem své „spodní nožičky“. Střed otáčení nemusí být nutně součástí bloku,
např. [(-1, -1), (-1, 0), (-1, 1), (0, 1)] je tetromino tvaru L, které se
otáčí kolem prázdného místa ve svém rohu.
Přestože se v grafickém rozhraní používají pouze tetromina (tedy klasické
tetrisové bloky), vaše řešení musí být obecné a fungovat s libovolnými tvary
bloků.
Poznámka: Protože za zeď považujeme i prostor „nad“ herní oblastí, může se
v mnoha případech stát, že blok, který se nově objevil, nebude možné otočit,
dokud se neposune o něco níže. Ačkoli reálné implementace tuto možnost
většinou nějak ošetřují, zde pro zjednodušení nic takového neděláme
a považujeme to za očekávané chování.
Position = tuple[int, int]
class Tetris:
Po inicializaci by měla být herní oblast prázdná, o zadaných rozměrech.
Není žádný padající blok a skóre je nastaveno na 0.
def __init__(self, cols: int, rows: int):
pass
Čistá metoda get_score vrátí aktuální skóre.
def get_score(self) -> int:
pass
Metoda-predikát has_block vrátí True právě tehdy, existuje-li
padající blok.
def has_block(self) -> bool:
pass
Metoda add_block přidá do hry padající blok na zadaných souřadnicích.
Pokud přidání bloku není možné (překrýval by se s již položenými
kostkami), metoda situaci nezmění a vrátí False; jinak vrátí True.
Metoda bude volána pouze tehdy, neexistuje-li žádný padající blok.
Seznam block nijak nemodifikujte. Pokud si ho hodláte někam uložit,
tak buďto zaříďte, aby se ani později nemodifikoval, nebo si vytvořte
jeho kopii.
Metoda left posune padající blok o jednu pozici doleva, je-li to možné.
Tato metoda, stejně jako všechny následující metody pohybu, bude volána
jen tehdy, existuje-li padající blok.
def left(self) -> None:
pass
Metoda right posune padající blok o jednu pozici doprava,
je-li to možné.
def right(self) -> None:
pass
Metoda rotate_cw otočí padající blok po směru hodinových ručiček o 90
stupňů, je-li to možné.
def rotate_cw(self) -> None:
pass
Metoda rotate_ccw otočí padající blok proti směru hodinových ručiček
o 90 stupňů, je-li to možné.
def rotate_ccw(self) -> None:
pass
Metoda down posune padající blok o jednu pozici směrem dolů.
Pokud takový posun není možný, kostky z padajícího bloku se napevno
umístí do herní oblasti; zcela zaplněné řádky se pak z oblasti vymažou
a skóre se zvýší o druhou mocninu počtu vymazaných řádků.
def down(self) -> None:
pass
Metoda drop shodí padající blok směrem dolů (o tolik pozic, o kolik je
to možné). Kostky z padajícího bloku se pak napevno umístí do herní
oblasti; zcela zaplněné řádky se pak z oblasti vymažou a skóre se zvýší
o druhou mocninu počtu vymazaných řádků.
def drop(self) -> None:
pass
Čistá metoda tiles vrátí seznam všech pozic, na nichž má být vykreslena
kostka – tedy jednak všechny položené kostky v herní oblasti, jednak
všechny kostky tvořící padající blok. Na pořadí pozic v seznamu nezáleží.
Tuto metodu používají jak testy pro ověření správnosti implementace,
tak grafické rozhraní pro vykreslení hry.
Tato kapitola přináší do jazyka dva nové prvky, které oba souvisí
s typy:
Typovou anotaci typ₁ | typ₂ | … | typₙ, která realizuje tzv.
součtové typy, kdy o nějaké hodnotě umíme říct, že je určitě
některého z vyjmenovaných typů, ale který konkrétně to bude se
rozhodne až za běhu programu.
Zabudovaný predikát isinstance(value, type), který rozhodne,
je-li hodnota value typu type. Tento predikát lze s výhodou
použít v kombinaci se součtovými typy, kdy se v programu
potřebujeme rozhodnout podle skutečného typu hodnoty value.
V těle podmíněného příkazu if isinstance(value, type) pak
platí, že hodnota value má i staticky (tzn. pro účely typové
kontroly programem mypy) přiřazen typ type.
Minulý týden jsme si, mimo jiné, ukázali algoritmus pro efektivní
hledání hodnoty v seřazeném seznamu, a to metodou půlení
intervalu. Dnes si ukážeme jinou implementaci téhož algoritmu:
místo cyklu použijeme koncovou rekurzi. Takto zapsaný algoritmus
nám poskytne trochu jinou perspektivu na známý problém a zároveň
připomene základní myšlenku rekurze, kterou již znáte z přednášky.
Při studiu této ukázky Vám doporučujeme otevřít si také ukázku
08/bin_tree.py a oba přístupy (iterativní z minulého týdne a
rekurzivní v tomto souboru) průběžně srovnávat.
Protože rekurzivní implementace bude potřebovat dodatečné
parametry, rozdělíme si ji na dva predikáty: bin_search_rec,
která provede samotné rekurzivní hledání, a bin_search, která
rekurzi pouze nastartuje (a slouží tak zejména jako příjemnější
rozhraní pro volání funkce bin_search_rec).
Chceme-li použít rekurzi, musíme problém formulovat tak, aby měl
jasně určené podproblémy (nebo podproblém), který je v nějakém
smyslu menší, než původní problém. Dále pak budeme chtít, aby bylo
jednoduché odpovědi na podproblémy zkombinovat tak, abychom
dostali odpověď na původní problém. V případě, kdy je podproblém
pouze jeden, je často možné použít navíc koncovou rekurzi:
výsledek (vhodně zvoleného) podproblému je přímo i výsledkem
celého problému. Koncová rekurze má proti té obecné dvě základní
výhody:
takto zapsaný výpočet lze provádět efektivně (bez použití
dodatečné paměti),
o koncové rekurzi se lépe uvažuje, protože má zvlášť
jednoduchou strukturu.
Na to, abychom „objevili“ v algoritmu vhodné podproblémy, trochu
si jej zobecníme: místo hledání v seznamu si jej zadefinujeme,
jako hledání v nějakém souvislém úseku daného seznamu: konkrétně
v polouzavřeném intervalu kde je dané parametrem
low a je dané parametrem high. Toto by nám již mělo
nápadně připomínat implementaci z minulého týdne.
Pro úplnost, predikát bin_search_rec odpovídá na otázku „je
hodnota value přítomna v seznamu records na některém indexu
z intervalu ?“
V řešení jednotlivých případů začneme od toho nejjednoduššího:
je-li vstupní interval prázdný, hodnota value se v něm jistě
nenachází. Tato podmínka je analogická ukončovací podmínce
cyklu while z iterativní verze. Vrátíme tedy hodnotu False
a jsme hotovi.
if low == high:
return False
Řešení ostatních případů záleží na tom, ve které části seznamu
se musí hodnota nacházet (je-li přítomna). Tyto případy jsou
analogické k případům, které iterativní verze ošetřovala
v těle cyklu. Nejprve si vybereme vhodný dělící bod
(zhruba uprostřed intervalu). Zejména platí, že
(v programu reprezentované proměnnou mid) vždy spadá do
intervalu .
mid = low + (high - low) // 2
Je-li tedy hledaná hodnota přímo na indexu mid, je určitě
v intervalu a tedy můžeme odpovědět True. Argument
proč to stačí je analogický k iterativní verzi.
if records[mid] == value:
return True
Jednoduché případy máme vyřešeny, nyní zbývají ty složitější:
totiž ty, které vedou na nějaký podproblém. Je-li hodnota na
indexu („uprostřed“ seznamu) menší než value, znamená
to, že je-li hodnota value v seznamu někde přítomna, musí to
být v horní části.
Kýžený podproblém je tedy „je hodnota value přítomna
v seznamu records na indexech z intervalu ?“ Je
zde dobře vidět i struktura koncové rekurze: odpověď na novou
otázku je zároveň odpovědí na tu původní (totiž „je value
přítomno v intervalu indexů “). Výsledek řešení
podproblému můžeme přímo, bez jakýchkoliv dalších úprav,
vrátit.
Zbývá poslední možnost: hodnota musí být v spodní části
prohledávaného intervalu, a tedy podproblém, který musíme
vyřešit, je „je hodnota value přítomna v intervalu indexů
?“
if records[mid] > value:
return bin_search_rec(records, value, low, mid)
Protože jsme pokryli všechny možnosti, do tohoto místa se již
program nemůže dostat. Toto naznačíme tvrzením False.
assert False
Samotný predikát bin_search se již pomocí bin_search_rec
vyjádří velice snadno: stačí zvolit interval tak, že
pokrývá právě všechny platné indexy seznamu records.
Protože řešený problém je identický jako minulý týden, budou i
testy identické.
def main() -> None: # demo
for low, high, count in test_parameters():
for records in sorted_lists(low, high, count, []):
for v in range(low - 1, high + 1):
assert bin_search(records, v) == (v in records)
result = []
for x in range(low, high):
result.extend(sorted_lists(x, high, count - 1, prefix + [x]))
return result
def test_parameters() -> list[tuple[int, int, int]]:
result = []
for high in range(10):
for low in range(high):
for count in range(0, 8):
result.append((low, high, count))
return result
V této ukázce budeme pracovat se stromy. Strom je datová
struktura, která se podobá zřetězenému seznamu, s jedním zásadním
rozdílem: uzly nemají následníka jednoho, ale několik. Podle toho,
kolik, dělíme stromy na binární (2 následníci), ternární (3
následníci), atd. Lze také uvažovat stromy s proměnným počtem
následníků (takovým se většinou říká n-ární). Počátečnímu uzlu
(tomu, který nemá ve stromě žádné předchůdce) často říkáme kořen.
Stromy sdílí se zřetězenými seznamy krom podobné struktury i jednu
velmi důležitou vlastnost: jsou to rekurzivní datové struktury.
Co to znamená? U seznamu to, že následník uzlu seznamu tvoří opět
seznam (navíc striktně menší seznam). A u stromu zase platí, že
každý následník je podstrom (striktně menší strom).
Tato struktura velmi dobře koresponduje s naší představou
o rekurzi: problém rozdělíme na podproblémy (pro každý podstrom
vznikne jeden) a dílčí výsledky nějak zkombinujeme na výsledek
celkový. Elementární (bázové) podproblémy pak tvoří stromy
o jediném uzlu (takové, které nemají žádné podstromy, známé též
jako listy), případně stromy prázdné (je-li to výhodné).
Jako první příklad na práci se stromy si naprogramujeme test na
přítomnost hodnoty ve stromě. Vstupem je (potenciálně prázdný)
strom a hledaná hodnota.
def search(tree: Tree | None, value: int) -> bool:
Aplikujeme nyní již snad dobře známý postup: nejprve vyřešíme
bázové (jednoduché) případy: je-li strom prázdný, hledaná
hodnota se v něm jistě nenachází (vracíme False).
if tree is None:
return False
Naopak, je-li hledaná hodnota uložena v aktuálním uzlu, můžeme
rovnou vrátit True.
if value == tree.value:
return True
Zbývají případy, které neumíme řešit přímo: víme ale, že je-li
hodnota ve stromě přítomna, musí to být v levém nebo v pravém
podstromě. Protože podstromy jsou menší (jednodušší) než celý
strom, jedná se o podproblémy, které můžeme řešit rekurzí.
Aplikujeme tedy predikát search na oba podstromy: hodnota je
ve stromě přítomna, je-li přítomna alespoň v jednom z jeho
podstromů.
return search(tree.left, value) or search(tree.right, value)
Nezbývá, než predikát search otestovat na několika jednoduchých
vstupech.
Analogií k seřazenému seznamu je takzvaný vyhledávací strom.
Tento má tu vlastnost, že všechny hodnoty uložené v levém
podstromě jsou menší nebo rovny hodnotě uložené v zkoumaném uzlu,
a naopak, hodnoty v pravém podstromě jsou větší nebo rovny.
Podobně jako v uspořádaném seznamu, lze ve vyhledávacím stromě
test na přítomnost hodnoty provést výrazně rychleji, než ve stromě
obecném.
def lookup(tree: Tree | None, value: int) -> bool:
Jednoduché případy jsou zcela stejné, jako při hledání
v obecném stromě.
if tree is None:
return False
if value == tree.value:
return True
Zajímavá změna se objeví v rekurzivním případě: podobně jako
při hledání půlením intervalu můžeme srovnáním hledané hodnoty
a hodnoty v aktuálním uzlu rozhodnout, ve kterém podstromě se
hledaná hodnota musí nacházet (je-li přítomna). Je-li hledaná
hodnota menší, než ta v aktuálním uzlu, víme jistě, že se
v pravém podstromě určitě nemůže objevit. Stačí nám tedy
vyřešit jediný podproblém, a to test na přítomnost hodnoty
v levém podstromě. Protože máme jediný podproblém, nabízí se
možnost použít koncovou rekurzi: musí ale navíc platit, že
řešení podproblému je přímo i řešením problému. Rozmyslete si,
že tomu tak skutečně je!
if value < tree.value:
return lookup(tree.left, value)
Opačný případ je zcela analogický: můžeme-li vyloučit
přítomnost hodnoty v levém podstromě, zbývá jediný podproblém,
který je navíc menší než ten aktuální (podstrom je jednodušší
než celý strom). Opět postupujeme koncovou rekurzí.
if value > tree.value:
return lookup(tree.right, value)
Mělo by být zřejmé, že jsme vyčerpali všechny možnosti,
program se do tohoto místa tedy nemůže dostat. Tuto skutečnost
opět deklarujeme tvrzením False.
assert False
Krom predikátu lookup zadefinujeme ještě jeden predikát: takový,
který zjistí, je-li nějaký strom korektním vyhledávacím stromem.
Predikát ale pro rozklad na podproblémy stačit nebude: lze
sestavit strom ze dvou korektních vyhledávacích stromů takový, že
výsledek nebude korektním vyhledávacím stromem, ale lokálně (jen
z jednoho vrcholu a jeho přímých následníků) to nebude lze poznat.
Třeba tento: Tree(5, Tree(2, leaf(1), leaf(10)), leaf(8)).
Musíme vyřešit silnější problém: takový, který nám umožní složit
správné řešení z vyřešených podproblémů. Jaké jsou lokální
vlastnosti korektního vyhledávacího stromu? Jsou to:
maximum levého podstromu je ≤ hodnota aktuálního uzlu,
minimum pravého podstromu je ≥ hodnota aktuálního uzlu,
levý i pravý podstrom jsou korektní.
Potřebujeme tedy funkci, která zjistí korektnost, minimum a
maximum daného (pod)stromu: víme už, že z těchto informací umíme
zjistit korektnost celého stromu. Na to, abychom mohli použít
rekurzi, musíme ještě zjistit minimum a maximum: za předpokladu,
že je strom korektní, platí:
minimum levého podstromu je zároveň minimum celého stromu,
maximum pravého podstromu je zároveň maximum stromu.
Všechny informace tedy umíme spočítat lokálně, z informacích
získaných řešením podproblémů. Můžeme tedy přistoupit
k rekurzivnímu řešení problému.
Abychom si trochu zjednodušili život, přidáme si umělý parametr:
příhodnou mez, kterou použijeme jako minimum i maximum, je-li
zadaný strom prázdný (takový strom totiž žádné přirozené meze
nemá). Tento postup nám oproti variantě s None ušetří spoustu
psaní.
Jako vždy, nejprve vyřešíme jednoduché případy: prázdný strom
je korektní (splňuje všechny požadavky). Zároveň nemá žádné
přirozené meze, proto použijeme tu, kterou nám volající předal
jako výchozí.
if tree is None:
return (True, bound, bound)
Je-li strom neprázdný, získáme vlastnosti levého i pravého
podstromu rekurzivním voláním.
Podle kritérií uvedených výše vypočteme, je-li strom jako
celek korektní.
this_ok = l_ok and r_ok and l_max <= tree.value <= r_min
Nyní nám stačí sestavit návratovou hodnotu. Není-li strom
korektní, nemusíme se správností mezí zabývat: žádný strom,
který má nekorektní podstrom, nemůže být korektní, bez ohledu
na meze svých podstromů.
return (this_ok, l_min, r_max)
Protože jsme potřebovali formulovat silnější problém, má funkce
is_correct_rec nesprávné rozhraní: zejména to není predikát
(výsledkem je n-tice, nikoliv bool), navíc má nežádoucí parametr
bound. Původně zamýšlený predikát ale už pomocí is_correct_rec
lehce zapíšeme:
Tato ukázka přinese oproti předchozím dvě rozšíření:
n-ární stromy (tedy takové, kde počet potomků jednoho uzlu není
předem omezen – potomky budeme ukládat do seznamu),
nepřímá (nebo vzájemná – mutual) rekurze, tedy situaci, kdy
nějaká funkce ve svém řešení používá k řešení menších
podproblémů funkci a naopak, využívá pro menší
podproblémy funkci .
Definice stromu se od předchozích liší pouze reprezentací
následníků. Protože se jedná o seznam, tento může být přirozeně
prázdný a není tedy potřeba pro neexistující následníky používat
None. Protože ale budeme chtít reprezentovat stromy, které
nemají hodnoty ve všech uzlech, objeví se None tentokrát jako
možná hodnota uzlu.
class Tree:
def __init__(self, value: int | None, children: list['Tree']):
self.value = value
self.children = children
Jaký problém tedy budeme řešit? Uvažme strom, který má dva typy
vnitřních uzlů (vnitřní uzly jsou ty, které mají nějaké
následníky): uzly typu „min“ a uzly typu „max“. Tyto jsou ve
stromě navíc rozvrženy tak, že uzel „max“ má následníky pouze typu
„min“ a naopak, uzel „min“ má následníky pouze typu „max“.
Bude výhodné o situaci uvažovat tak, že to, které uzly budou „min“
a které „max“ bude záviset od jejich vzdálenosti od kořene, a od
toho, je-li kořen typu „min“ nebo typu „max“. Krom vnitřních uzlů
má strom listy: to jsou právě ty uzly, které již žádné
následníky nemají. Náš „minmax“ strom bude v listech obsahovat
celá čísla. Hodnotu vnitřního uzlu pak spočítáme jako minimum
(je-li to uzel typu „min“) nebo maximum (je-li typu „max“) hodnot
všech jeho následníků.
Funkce nazveme tree_minmax (kořen je typu „min“) a tree_maxmin
(kořen je typu „max“). Z popisu výše je zřejmé, že je-li kořen
stromu typu „min“, budou kořeny všech podstromů typu „max“:
rekurzivní volání proto bude vždy používat opačnou funkci.
def tree_minmax(tree: Tree) -> int:
Jako vždy, nejprve vyřešíme jednoduché případy: konkrétně zde
případ, kdy je uzel listem (má hodnotu nastavenu přímo).
if tree.value is not None:
return tree.value
Ze seznamu potomků (podstromů) vytvoříme seznam jejich hodnot
použitím funkce tree_maxmin. Z tohoto seznamu již lehce
získáme výsledek: protože kořen je typu „min“, bude to minimum
z hodnot všech následníků.
return min([tree_maxmin(child) for child in tree.children])
Funkce tree_maxmin je vůči tree_minmax zcela symetrická:
def tree_maxmin(tree: Tree) -> int:
if tree.value is not None:
return tree.value
return max([tree_minmax(child) for child in tree.children])
Mějme následující problém: na vstupu je zadaný seznam čísel a
počáteční index. V každém kroku k aktuálnímu indexu přičteme
hodnotu na tomto indexu uloženou. Mohou nastat tyto možnosti:
index po konečném počtu iterací „vypadne“ z rozsahu seznamu,
výpočet se zacyklí a bude navštěvovat nějakou množinu indexů
„donekonečna“.
Zajímá nás která možnost nastane, a v případě 2 také délka cyklu,
který se bude opakovat (t.j. velikost množiny indexů, které budou
v cyklu navštěvovány).
V této ukázce naprogramujeme čistou funkci cycle, která tento
problém řeší. Problém rozdělíme na dvě části: nejprve zjistíme,
která z možností nastala. Poté, je-li to možnost 2, zjistíme délku
cyklu. Jako cvičení si můžete zkusit implementovat verzi, která
problém vyřeší na jeden průchod, za cenu uložení dodatečné
informace.
Použijeme koncovou rekurzi, ale tato bude mít trochu jiný
charakter, než v předchozích ukázkách: problém, který řešíme, nemá
žádnou jasnou (statickou) strukturu podproblémů, a nemůžeme tedy
použít jednoduchou strukturální rekurzi.
Hlavní myšlenka rekurze nicméně zůstane zachována: nejprve
vyřešíme elementární případy, kdy je odpověď na první pohled
jasná. Ty zbývající musíme nějakým vhodným způsobem převést na
jednodušší instance: to, v čem se tento příklad liší od těch
předchozích je, že nemáme k dispozici jasného kandidáta na vhodnou
jednodušší instanci (chybí nám již zmiňovaná struktura
podproblémů).
Jak tedy měřit jednoduchost? Neexistuje žel žádná univerzální
odpověď ani univerzální postup, a „uvidět“ vhodné řešení vyžaduje
určitý cvik.
Zaměřme se tedy na funkci cycle_detect, která bude zjišťovat,
jestli se výpočet zacyklí nebo nikoliv. V tomto případě se jako
vhodné měřítko jednoduchosti jeví kritérium „kolik indexů jsme
ještě během výpočtu nenavštívili?“. Jednou z indicií je i to, že
když je tento počet 0, stojíme před elementárním případem – index
je buď platný (a tedy navštívený: našli jsme cyklus) nebo
neplatný. Pro žádný složitější případ nezbývá prostor. Máme tedy
jakousi záruku, že dokážeme-li postupně toto číslo snižovat, dříve
nebo později narazíme na elementární problém. To je dobře.
Z praktického hlediska je ale lepší pamatovat si množinu použitých
indexů, nikoliv těch nepoužitých: to ale není problém, protože
tyto množiny jsou ve velmi jednoduchém vztahu (jsou vzájemnými
doplňky v množině všech platných indexů). Přidáme-li index do
množiny navštívených indexů, je to totéž, jako bychom jej odebrali
z množiny indexů nenavštívených.
Funkce cycle_detect tedy bude mít 3 parametry: samotný seznam
čísel, aktuální index a množinu již navštívených indexů. Výsledkem
pak bude libovolný index, který se během výpočtu zopakoval
(existuje-li, jinak None).
Podobně jako v předchozím, nejprve vyřešíme jednoduché
případy: je-li index mimo meze seznamu numbers, není co
řešit: vracíme None.
if index < 0 or index >= len(numbers):
return None
Naopak, je-li index přítomen v množině visited, víme, že
se během výpočtu zopakoval a můžeme jej tedy vrátit.
if index in visited:
return index
Ve zbývajících případech nemůžeme přímo rozhodnout. Můžeme ale
aktuální index označit za navštívený, provést krok výpočtu, a
novou instanci problému prohlásit za jednodušší: díky tomu
můžeme zbytek práce bezpečně delegovat na rekurzivní volání
cycle_detect.
Vzhledem k předchozímu víme, že index dosud nebyl
navštívený, tedy jeho přidáním se množina visited zvětší
o 1, a tedy počet nenavštívených indexů o 1 klesne. Víme tedy,
že takto formulovaná nová instance je blíže elementárnímu
případu než ta stávající.
Funkce cycle_length je ještě o něco zapeklitější. Nejlepší míra
„jednoduchosti“ je zde počet kroků, které musíme provést, abychom
se z indexu index dostali na index start. Tato informace ale
není vůbec nikde ve funkci přítomna, a není ani jasné, že je tento
počet konečný. Skutečně, vhodnou volbou parametrů můžeme způsobit,
že funkce cycle_length nikdy neskončí (například numbers = [1,
0], start = 0, index = 1).
Z pátého týdne ale víme, že funkce mohou mít vstupní podmínku:
toho zde s výhodou využijeme. Aby funkce cycle_length smysluplně
fungovala, musí platit, že index start je z indexu index
dosažitelný konečným počtem kroků výpočtu – toto kritérium tedy
zvolíme jako vstupní podmínku.
Protože budeme začínat v situaci, kdy platí index == start,
ale ještě jsme žádný krok výpočtu neprovedli (count je 0),
musíme si elementární případ pohlídat: ten totiž nastane pouze
je-li count alespoň 1.
if count and index == start:
return count
Nyní zbývá vyřešit rekurzivní volání. Ze vstupní podmínky
víme, že z index do start se dostaneme konečným počtem
kroků výpočtu. Provedeme-li tedy krok výpočtu z indexu
index, tato vzdálenost se o jedna zmenší. Protože byla na
začátku konečná (byla splněna vstupní podmínka), bude jistě
konečná i po provedení kroku výpočtu: vstupní podmínka funkce
cycle_length je i v nové situaci splněna (toto je velmi
důležité ověřit!) a můžeme tedy provést rekurzivní volání.
Zároveň víme, že se jedná o „jednodušší“ instanci (vzdálenost
se nutně zmenšila).
Nyní už je jednoduché funkce zkombinovat do funkce cycle.
Všimněte si, že výstupní podmínka funkce cycle_detect nám
zaručuje splnění vstupní podmínky funkce cycle_length.
Napište čistou funkci, která dostane jako parametr instanci výše
uvedeného stromu reprezentující nějaký aritmetický výraz, a vrátí
seznam řetězců, ve kterém je tento výraz zapsán v postfixové (rpn)
notaci. Každý prvek bude odpovídat právě jednomu uzlu vstupního
stromu.
Uvažme n-ární strom, který má v uzlech uloženu volitelnou hodnotu
typu int.
class Tree:
def __init__(self, children: list["Tree"]):
self.value: int | None = None
self.children = children
Napište proceduru, která obdrží instanci výše popsaného stromu, a
vyplní atributy value všech jeho uzlů tak, aby byl v každém uzlu
uložen celkový počet jeho potomků (tedy včetně nepřímých).
Správné řešení má složitost lineární vůči počtu uzlů stromu.
Třídy IntTree, StrTree a TupleTree reprezentují postupně
stromy, které mají v uzlech uložená celá čísla (int), řetězce
(str) a dvojice číslo + řetězec.
Klíče tohoto slovníku jsou čísla vrcholů a hodnoty jsou seznamy
čísel jejich přímých potomků. Nejprve napište predikát, který
ověří, že se jedná o korektně zadaný strom, tedy:
obsahuje kořen (uzel číslo 1),
každý vrchol se v seznamech potomků objevuje právě jednou,
s výjimkou kořene, který se zde neobjevuje vůbec,
žádný uzel není svým vlastním (přímým) potomkem.
def is_tree(tree: TreeDict) -> bool:
pass
Dále napište čistou funkci make_tree, která ze zadaného
„slovníkového“ stromu tree vytvoří instanci třídy Tree tak,
aby reprezentovala stejný strom. Vstupní podmínkou je, že tree
je korektní strom, tzn. platí is_tree(tree).
class Tree:
def __init__(self, value: int, children: list["Tree"]):
self.value: int = value
self.children = children
Pro účely tohoto cvičení musíme trochu pozměnit zápis stromu do
tříd. Protože budeme strom měnit „na místě“, musí být prázdný i
neprázdný strom reprezentován stejným typem. Proto si jej
rozdělíme na třídy Node a Tree, které budou hrát podobnou roli
jako jejich protějšky v zřetězeném seznamu. Tyto třídy nijak
nemodifikujte.
class Node:
def __init__(self, value: int,
left: 'Node | None',
right: 'Node | None'):
self.value = value
self.left = left
self.right = right
Napište proceduru, která na vstupu dostane instanci výše popsaného
stromu tree a množinu celých čísel keep a ze stromu tree
odstraní všechny vrcholy (uzly), kterých hodnota v množině keep
chybí. Spolu s vrcholem odstraňte i celý podstrom, který v něm
začíná. Správné řešení má složitost lineární vůči počtu uzlů
původního stromu.
Binární halda je binární strom, který má dvě speciální vlastnosti
uvedené níže. V tomto příkladu budeme kontrolovat pouze tu druhou,
totiž vlastnost haldy:
každé patro je plné (s možnou výjimkou posledního),
hodnota každého uzlu je větší nebo rovna hodnotě libovolného
jeho potomka.
Predikát is_heap rozhodne, splňuje-li vstupní strom tuto druhou
vlastnost.
class Tree:
def __init__(self, left: 'Tree | None',
right: 'Tree | None') -> None:
self.left = left
self.right = right
def leaf() -> Tree:
return Tree(None, None)
AVL strom je binární strom, který:
je vyhledávací, tzn. splňuje vlastnost popsanou v ukázce
d3_lookup, a zároveň
pro každý jeho uzel platí abs(l_height - r_height) ≤ 1, kde
l_height a r_height jsou výšky levého a pravého podstromu
daného uzlu.
Napište predikát, který ověří, že vstupní strom má tuto druhou
vlastnost (je-li zároveň stromem vyhledávacím ověřovat nemusíte).
Pokuste se vlastnost ověřit jediným průchodem stromu (tedy každý
uzel navštivte pouze jednou – naivní řešení, kdy opakovaně
počítáte výšky průchodem podstromů není příliš uspokojivé).
Binární rozhodovací diagram (anglicky „binary decision diagram“,
zkráceně „BDD“) je datová struktura, která umožňuje efektivně
kódovat formule výrokové logiky, například:
Protože budeme takto zapsané funkce pouze vyhodnocovat, můžeme se
na BDD dívat jako na binární strom,13 který má ve vnitřních uzlech
názvy proměnných a v listech pravdivostní hodnoty (budeme je
reprezentovat hodnotami 0 a 1). BDD pro výše uvedenou formuli
může vypadat například takto:
BDD vyhodnotíme tak, že začneme v kořenu, a v každém uzlu se
rozhodneme podle pravdivosti proměnné, kterou je tento uzel
označený: je-li pravdivá, pokračujeme doprava, jinak doleva.
Výsledkem je hodnota, kterou najdeme v takto nalezeném listu.
Srovnejte tabulku pravdivostních hodnot:
a
b
c
b ∧ c
a ∨ (b ∧ c)
a ∧ c
φ
0
0
0
0
0
0
1
0
0
1
0
0
0
1
0
1
0
0
0
0
1
0
1
1
1
1
0
0
1
0
0
0
1
0
0
1
0
1
0
1
1
1
1
1
0
0
1
0
0
1
1
1
1
1
1
1
class BDD:
def __init__(self, val: str, left: 'BDD | None',
right: 'BDD | None') -> None:
self.val = val
self.left = left
self.right = right
Naprogramujte čistou funkci, která vyhodnotí zadané BDD pro dané
ohodnocení proměnných. Předpokládejte, že každý vnitřní uzel má
oba potomky. Hodnoty proměnných jsou zadané množinou true_vars:
je-li název proměnné v této množině, proměnná je pravdivá, jinak
nikoliv. V listech jsou uloženy řetězce "0" (výsledek je
False) nebo "1" (výsledek je True).
V praxi se obvykle používají tzv. redukované BDD, kde jsou některé podstromy vhodně sloučeny, a to tak, aby se nezměnil výsledek vyhodnocení. Na samotný proces vyhodnocování tato úprava nemá žádný vliv.
class Tree:
def __init__(self, left: 'Tree | None',
right: 'Tree | None') -> None:
self.left = left
self.right = right
def leaf() -> Tree:
return Tree(None, None)
Napište čistou funkci, která pro vstupní binární strom spočítá
průměrnou délku větve (cesty od kořene k listu).
K řešení úlohy je postačující projít strom jen jednou.
V tomto příkladu se vrátíme k problému 09/cycle.py z minulého
týdne. Připomeňme si základní strukturu:
vstupem je seznam čísel, a počáteční index,
v každém kroku výpočtu se číslo na aktuálním indexu k tomuto
indexu přičte, čím vznikne nový index.
Tento proces se může, ale nemusí, zacyklit. Ve verzi z minulého
týdne jsme pouze rozhodovali, která možnost nastane. Tentokrát
bude problém postaven trochu jinak: všechna čísla v seznamu budou
kladná, a v každém kroku máme možnost rozhodnout se, budeme-li
číslo přičítat nebo odečítat.
Naším cílem bude zjistit, nejen existuje-li nějaký cyklus
(sekvence rozhodnutí vlevo/vpravo taková, že ji lze donekonečna
opakovat), ale navíc existuje-li takový, že navštíví všechny
platné indexy. Není těžké si domyslet, že na počátečním indexu
vůbec nezáleží, protože hledaný cyklus prochází každým indexem, a
tedy jej můžeme z formulace problému vypustit.
Problém budeme řešit jak jinak než rekurzí. Hlavní část řešení
zastřešuje predikát solve_rec, s následovnými parametry:
numbers je zadaná „hrací plocha“,
index je současně zkoumaný index,
goal je index, ke kterému chceme dojít, a konečně
to_visit je množina dosud nenavštívených indexů.
Predikát odpovídá na otázku: lze se z indexu index dostat na
index goal tak, že každý index z to_visit se použije právě
jednou? Zřejmě si dovedete představit, že jakmile vyřešíme tento
problém, dokážeme již původní otázku na přítomnost cyklu lehce
vyjádřit jako jeho instanci (chceme se dostat z nějakého indexu na
tentýž index a použít k tomu právě všechny platné indexy).
Vyřešíme nejprve jednoduché případy. Vypadneme-li z rozsahu
indexů, jistě se nám už k indexu goal nepodaří dojít a
odpovídáme zamítavě.
if index < 0 or index >= len(numbers):
return False
V případě, že jsme na indexu goal a množina to_visit je
prázdná, je zřejmé, že odpověď je True (jsme tam, kde máme
být, a máme se tam dostat bez použití jakéhokoliv jiného
indexu).
if index == goal and not to_visit:
return True
Konečně případ, kdy se nacházíme na indexu, který není cílem,
a zároveň jej již nelze použít (není přítomen v to_visit):
zamítáme. Speciálním případem této podmínky je i stav, kdy je
množina to_visit prázdná.
if index not in to_visit:
return False
V ostatních případech nelze přímo rozhodnout. Jednodušší
instance sestavíme tak, že aktuální index odebereme
z to_visit a posuneme se buď doleva (index_left) nebo
doprava (index_right). Do goal vede přípustná cesta tehdy,
když taková existuje v alespoň jedné z takto sestrojených
instancí. Instance jsou jednodušší, protože množina to_visit
se zmenšila, a případ, kdy je prázdná je vždy jednoduchý (viz
výše).
remaining = to_visit - {index}
index_left = index - numbers[index]
index_right = index + numbers[index]
return (solve_rec(numbers, index_left, goal, remaining) or
solve_rec(numbers, index_right, goal, remaining))
Jak již bylo naznačeno, původní problém již lehce zapíšeme jako
instanci problému, který řeší predikát solve_rec.
def solve(numbers: list[int]) -> bool:
indices = [i for i in range(len(numbers))]
return solve_rec(numbers, 0, 0, set(indices))
Řešení jako obvykle otestujeme na jednoduchých příkladech.
def main() -> None: # demo
assert not solve([1])
assert solve([1, 1])
assert not solve([1, 0, 1])
assert not solve([1, 1, 1])
assert solve([1, 1, 2])
assert not solve([1, 2, 1])
assert solve([1, 1, 1, 3])
assert solve([3, 1, 1, 1])
assert solve([2, 1, 2, 2, 1])
assert not solve([2, 2, 2, 2, 2])
assert not solve([2, 2, 1, 2])
V tomto příkladu se vrátíme k „minmax“ stromům z 09/minmax.py a
zejména k jejich praktické aplikaci. Strom už nicméně nebudeme
reprezentovat explicitně jako datovou strukturu, budeme jej vždy
konstruovat „podle potřeby“ lokálně, v rámci rekurzivního řešení
nějakého problému.
Problém, který budeme řešit je jak vyhrát (nebo aspoň neprohrát)
piškvorky na ploše (v angličtině známé jako „tic-tac-toe“).
Tato hra je dost jednoduchá na to, abychom dokázali řešení najít i
celkem naivně.
Jak si jistě pamatujete z minula, v „minmax“ stromu se střídají
„min“ uzly a „max“ uzly: my teď do každého uzlu umístíme hrací
plán: do uzlů typu „max“ takový, kde jsme na tahu my (hráč
s křížky) a do uzlů typu „min“ pak ty, kde je na tahu hráč
s kolečky (náš protivník). Zbývá nám ještě ohodnotit listy, které
budou reprezentovat ukončené hry (některý hráč vyhrál, nebo je
plocha již zaplněná a došlo tedy k remíze). To provedeme tak, že
remízu ohodnotíme nulou (neutrální výsledek), výhru křížků
ohodnotíme (pozitivní) a výhru koleček (negativní)
výsledek. Takový strom můžeme zřejmě nakreslit z libovolné herní
pozice. Například (poslední tah je vždy vybarven, u vnitřních uzlů
uvádíme jejich vypočtené hodnoty):
Jak nám takový strom pomůže vyhrát? Střídající se minima a maxima
v jednotlivých patrech stromu odpovídají nejlepším možným tahům
příslušného hráče: dojdeme-li do listu s hodnotou -1, znamená to,
že kolečka vyhrála (tomuto hráči budeme odteď říkat „min“). Cílem
hráče „min“ je tedy dostat se do listu ohodnoceného -1. Naopak,
hráč s křížky (bude se jmenovat „max“) se pokouší dostat do listu
ohodnoceného +1. Toto odpovídá elementárním případům rekurze.
Stojí-li hráč před posledním rozhodnutím (uvažme třeba
nejspodnější případ z druhého sloupce obrázku, kde se hráč „min“
rozhoduje mezi dvěma políčky), vybere si tu z nich, která povede k
výhře (je-li to možné), případně k remíze. Je vidět, že to
odpovídá právě následníkovi s nejmenší hodnotou (pro hráče „min“,
u hráče „max“ je tomu přesně naopak). Totéž samozřejmě platí i o
patro výš, a tak dále, až ke kořeni.
Abychom mohli takový pomyslný „minmax“ strom prohledat, musíme
umět reprezentovat jeho jednotlivé vrcholy: ty neobsahují nic
jiného, než herní pozice. Ty budeme reprezentovat dvourozměrným
seznamem čísel. Prázdná políčka budou mít hodnotu 0, hráči pak
budou používat „svoji“ hodnotu: hráč „min“ dostane -1 a hráč „max“
+1. Jednotlivý tah pak budeme reprezentovat jako dvojici
souřadnic, každou z rozsahu .
Plan = list[list[int]]
Move = tuple[int, int]
První pomocnou funkci, kterou si zadefinujeme, bude čistá funkce
put, která dostane plán, souřadnice tahu, a hráče, a vytvoří nový
plán takový, kde zadaný hráč obsadil zadané políčko. Vstupní
podmínkou je, že políčko bylo prázdné.
def put(plan: Plan, where: Move, player: int) -> Plan:
x, y = where
assert plan[y][x] == 0
plan = [row.copy() for row in plan]
plan[y][x] = player
return plan
Čistá funkce list_empty vytvoří seznam všech přípustných tahů
(tzn. souřadnice všech prázdných políček v předané hrací ploše).
def list_empty(plan: Plan) -> list[Move]:
return [(x, y)
for x in range(3)
for y in range(3)
if not plan[y][x]]
Další (opět čistá) funkce bude line, která na vstupu dostane
počáteční souřadnice (parametry x a y) a „směr“ (parametry
dx a dy, které udávají požadovaný přírůstek na dané
souřadnici). Z těchto spočítá, je-li celá takto popsaná „čára“
obsazena týmž hráčem. Pokud ano, vrátí identifikátor hráče, jinak
nulu. Tato situace zřejmě odpovídá (nějaké) vítězné pozici.
def line(plan: Plan, x: int, y: int, dx: int, dy: int) -> int:
player = plan[y][x]
for n in range(1, 3):
if plan[y + dy * n][x + dx * n] != player:
return 0
return player
Následuje pomocná funkce, která vrátí svůj první nenulový
parametr,14 existuje-li takový (jinak vrátí nulu).
def either(a: int, b: int) -> int:
return a if a else b
Poslední pomocnou funkcí je winner, která rozhodne, zda některý
hráč již vyhrál, a pokud ano, který. Určitě to není nejkrásnější
funkce v historii funkcí, ale účel plní a je relativně kompaktní
(a to je občas také žádoucí).
def winner(plan: Plan) -> int:
player = 0
for v in range(3):
player = either(player, line(plan, v, 0, 0, 1))
player = either(player, line(plan, 0, v, 1, 0))
player = either(player, line(plan, 0, 0, 1, 1))
player = either(player, line(plan, 2, 0, -1, 1))
return player
Tím jsme vybaveni k implementaci samotného rekurzivního
prohledávání „minmax“ stromu hry tic-tac-toe.
Jak jsme již zvyklí, vyřešíme nejprve jednoduché případy,
totiž ty, kdy se nacházíme v listu. Listy jsou dvojího typu:
některý hráč vyhrál, nebo je pole již plné a nastala remíza.
won = winner(plan)
empty = list_empty(plan)
moves = []
if won or len(empty) == 0:
return (won, None)
Nejsme-li v listu, musíme prohledat následníky. Následník se
od aktuálního vrcholu odlišuje tím, že hráč, který je na tahu,
do některého volného pole umístí svůj symbol. Následníků je
právě tolik, kolik je volných políček. Nesmíme zapomenout, že
na tahu bude v rekurzivním volání opačný hráč, než je ten
současný. Krom skóre, které danému uzlu přisoudí rekurzivní
volání si zapamatujeme i tah, který do tohoto uzlu vedl.
for move in empty:
score, _ = decide(put(plan, move, player), -player)
moves.append((score, move))
Nyní již máme výsledky pro všechny následníky: vybereme ten
nejlepší možný – hráč „max“ ten maximální, zatímco hráč „min“
ten minimální. Všimněte si, že vybíráme ze seznamu, který
obsahuje dvojice (skóre, tah). Je-li několik ekvivalentních
možností (mají stejné skóre), hráč „min“ vybere ten s
nejmenšími a hráč „max“ ten s největšími souřadnicemi. Protože
na konkrétní volbě nezáleží, můžeme si tuto zápisovou zkratku
na tomto místě dovolit.
return max(moves) if player > 0 else min(moves)
Tím je hra tic-tac-toe vyřešena: máme algoritmus, který hraje
„nejlépe, jak je to možné“ – může-li v nějaké pozici vynutit
výhru, nebo alespoň remízu, decide vybere právě takové tahy, aby
ji skutečně vynutil.
Výjimečně si krom jednoduchých automatických testů přidáme i
možnost hry vypisovat na obrazovku. Pomocná procedura draw přidá
do rozpracovaného obrázku hry další tah.
def draw(plan: Plan, game_rows: list[list[str]]) -> None:
for i in range(min(len(plan), len(game_rows))):
game_row = game_rows[i]
game_row.append(' │ ' if game_row else ' ')
for cell in plan[i]:
game_row.append('×' if cell > 0 else
'○' if cell < 0 else '_')
A konečně procedura play nechá hrát strategii decide samu
proti sobě a výsledek nakreslí. Parametry jsou počáteční pozice a
hráč, který je na tahu. V parametru game si funkce udržuje
„obrázek“ hry, který na konci vypíše. Všimněte si, že tato funkce
s výhodou využívá koncové rekurze.
První dva testy odpovídají obrázku ze začátku příkladu.
Ty zbývající nejsou příliš intuitivní (proto jsme si
nechali hry vykreslovat), nicméně odpovídají konkrétním
volbám, které algoritmus provede.
V Pythonu by bylo lze stejného efektu docílit použitím operátoru or, nicméně se jedná o docela atypickou vlastnost jazyka, proto se zde takovému použití raději vyhneme.
Výrokovou logiku jistě znáte, například z předmětu MZI. To co
možná nevíte je, že každou formuli výrokové logiky lze přepsat do
obzvláště jednoduchého tvaru: takzvané konjunktivní normální
formy. V této formě se formule skládá ze závorek (klauzulí),
které jsou spojeny konjunkcí. V každé závorce je pak disjunkce
literálů: proměnných, nebo jejich negací. Například:
To, jak se formule do této podoby převede nás teď nemusí zajímat
(někdy později v průběhu studia to nejspíš ještě zjistíte), co je
důležité je, že nám stačí pracovat s formulemi tohoto tvaru.
Jak takové formule reprezentovat v programu? Vybudujeme si vhodné
typy odspodu, tzn. od samotných proměnných, které budeme
reprezentovat písmeny:
Variable = str
Literál budeme reprezentovat dvojicí: krom proměnné si potřebujeme
pamatovat, je-li literál pozitivní (pozitivní je, když proměnné
nepředchází negace): na toto nám stačí hodnota typu bool.
Literal = tuple[Variable, bool]
Dalším útvarem je klauzule, tedy disjunkce nějakého počtu
literálů:
Clause = list[Literal]
A konečně samotná formule, která je konjunkcí klauzulí:
Formula = list[Clause]
Zbývá poslední typ, který budeme potřebovat, a tím je valuace:
přiřazení pravdivostních hodnot jednotlivým proměnným.
Valuation = dict[str, bool]
Problém, který budeme řešit se jmenuje splnitelnost: bude nás
zajímat, existuje-li valuace taková, že se zadaná formule
vyhodnotí na True. Nejprve si ale naprogramujeme jednodušší
funkci: vyhodnocení formule, kterého vstupem je nějaká formule a
valuace proměnných, a výsledkem je pravdivostní hodnota. Budeme
navíc ale uvažovat i případ, kdy valuace není úplná, tzn. některé
proměnné nemají pravdivostní hodnotu určenu. V takovém případě
můžou nastat tři případy:
formule je pravdivá bez ohledu na nepřiřazené proměnné
(v každé klauzuli je alespoň jeden splněný literál),
formule je nepravdivá: existuje klauzule, která obsahuje pouze
přiřazené proměnné a zároveň není splněna,
o pravdivosti nelze rozhodnout: některou klauzuli se nepovedlo
splnit, ale tato klauzule obsahuje nerozhodnutou proměnnou.
Funkce evaluate bude v těchto situacích vracet postupně True
(určitě splněno), False (určitě nesplněno) a None (nevíme).
Formuli budeme vyhodnocovat po jednotlivých klauzulích.
Výsledek pro každou z nich může být, podobně jako pro celou
formuli, „splněna“, „nesplněna“ nebo „nelze říct“.
for clause in phi:
satisfied = False
undecided_literal = False
for variable, positive in clause:
if variable not in valuation:
undecided_literal = True
elif valuation[variable] == positive:
satisfied = True
break
V případě, že se klauzuli nepovedlo splnit, musíme
rozlišit dva případy: jestli tato obsahovala nerozhodnutý
literál (příslušná proměnná nemá přiřazenu pravdivostní
hodnotu), výsledek pro klauzuli je „nelze říct“ a
pokračujeme ve vyhodnocování (může se totiž ještě objevit
klauzule, která formuli rozhodne v záporu). Jsou-li ale
všechny proměnné v klauzuli přiřazené, víme, že formule
jako celek se vyhodnotí na False a tento výsledek můžeme
rovnou vrátit.
if not satisfied:
if undecided_literal:
undecided_clause = True
else:
return False
Žádná klauzule se nevyhodnotila na False, pro formuli jako
celek zbývají tedy pouze možnosti „splněna“ nebo „nelze říct“.
Druhá možnost nastane v případě, kdy se nám některou klauzuli
nepodařilo rozhodnout.
return None if undecided_clause else True
Dále budeme potřebovat (čistou) funkci, která nám z formule získá
množinu všech proměnných, které se ve formuli objevují.
def variables(phi: Formula) -> set[str]:
var_set: set[str] = set()
for clause in phi:
for var, _ in clause:
var_set.add(var)
return var_set
Poslední pomocnou funkcí (opět čistou) bude extend, která
do valuace přidá novou proměnnou. Vstupní podmínkou je, že tato
proměnná ještě ve valuaci hodnotu přiřazenou nemá.
def extend(val: Valuation, var: str, value: bool) -> Valuation:
assert var not in val
new = val.copy()
new[var] = value
return new
Nyní již můžeme přistoupit k samotnému řešení problému: možná si
pamatujete pravdivostní tabulky – jejich konstrukcí lze
jednoduše zjistit, je-li formule splnitelná. K tomu nám totiž
stačí nalézt splňující přiřazení (tedy takové, při kterém se
formule vyhodnotí na True). Pro vypadá pravdivostní tabulka takto:
0
0
0
0
0
0
1
0
0
1
0
1
0
1
1
1
1
0
0
0
1
0
1
1
1
1
0
0
1
1
1
1
Potřebujeme tedy algoritmus, který takovou tabulku sestrojí a
najde první řádek, kde formuli jako celku náleží hodnota 1 (neboli
True). Jak již jistě tušíte, použijeme rekurzi. Budeme si přitom
předávat dvě pomocné hodnoty: seznam proměnných, jejichž
pravdivost ještě potřebujeme rozhodnout, a částečnou valuaci,
kterou budeme postupně budovat. Význam predikátu satisfiable_rec
je „lze přiřazení valuation doplnit tak, aby formuli splnilo?“
Jako obvykle, nejprve vyřešíme jednoduchý případ, totiž ten,
kdy již formuli dokážeme rozhodnout. Tento případ zejména
nastane, je-li již přiřazení valuation kompletní a tedy
seznam to_decide prázdný.
Může se ale stát, že formuli dokážeme rozhodnout i přesto, že
jsme dosud nepřiřadili pravdivostní hodnoty všem proměnným.
Toto odpovídá třeba hned první dvojici řádků tabulky výše: na
hodnotě vůbec nezáleží, a při vyhodnocování druhého sloupce
prvního řádku zjistíme, že „tudy cesta nevede“: můžeme rovnou
skočit na řádek třetí.
result = evaluate(phi, valuation)
if result is not None:
return result
V případě, že zatím rozhodnout nelze, z to_decide vybereme
proměnnou, které následně přisoudíme pravdivostní hodnotu.
var = to_decide.pop()
Vybrané proměnné můžeme přisoudit hodnotu True nebo False,
čím dostaneme dvě (striktně úplnější) valuace: nazveme je
val_true a val_false.
val_true = extend(valuation, var, True)
val_false = extend(valuation, var, False)
Konečně přiřazení valuation lze na splňující přiřazení
doplnit právě tehdy, když lze takto doplnit alespoň jedno
z rozšířených přiřazení val_true nebo val_false. Zároveň
je zřejmé, že instance, které řešíme rekurzí jsou jednodušší:
zbývá o jednu nerozhodnutou proměnnou méně.
return (satisfiable_rec(phi, to_decide.copy(), val_true) or
satisfiable_rec(phi, to_decide, val_false))
Není již těžké si uvědomit, že formule je splnitelná právě když
lze prázdnou valuaci rozšířit na valuaci splňující:
Tím jsme hotovi, implementaci si ještě na několika formulích
otestujeme. Aby se nám formule trochu lépe četly, zadefinujeme si
pro jejich vytváření dvě jednoduché pomocné funkce (positive a
negative).
† V této ukázce přidáme oproti předchozím několik novinek. Nejprve
si ale představme problém, který budeme řešit. Možná znáte hru „15
puzzle“ – hraje se s 15 posuvnými kameny v rámu o rozměru –
jedno místo tedy zůstává volné a umožňuje kameny posouvat. My
budeme řešit o něco menší variantu této hry: 8 kamenů v rámečku
. Na kamenech může být třeba obrázek, ale tradiční varianta,
kterou budeme používat i my, má kameny očíslované od 1 do 8.
Vyřešený rébus má tedy tuto podobu:
Hra se hraje tak, že dostaneme pole nějak pomíchané a snažíme se
sestavit jej do podoby nakreslené výše. K dispozici máme vždy
několik tahů – můžeme si vybrat, který sousední kámen do prázdného
políčka přemístit. Protože hra je ve své klasické podobě
realizovaná fyzicky, přesouvat můžeme kameny pouze ve 4 směrech:
nahoru, dolů, doleva a doprava. Příklad krátké hry:
Každý přípustný počáteční stav hry (konfigurace) má mnoho řešení:
my budeme odpovídat na otázku, jak dlouhé je to nejkratší15
(s nejmenším počtem kroků). Nejprve si zadefinujeme několik
užitečných typů a pomocných funkcí. Uspořádání rámečku (krabičky)
budeme reprezentovat lineárním seznamem, a to tak, že vyřešená hra
bude mít tvar [0, 1, 2, 3, 4, 5, 6, 7, 8]: hrací pole budeme
odečítat ze seznamu po řádcích, vždy zleva doprava, prázdné
políčko reprezentujeme nulou. Souřadnice políčka budou dvojice
čísel z rozsahu , přičemž je levý horní roh.
Box = list[int]
Position = tuple[int, int]
Tahy budeme reprezentovat jako pohyb volného políčka (rozmyslete
si, že se jedná o ekvivalentní, ale úspornější popis, než si
pamatovat který kámen tahal kterým směrem). Tento pohyb budeme
zapisovat jako (dx, dy) – posuv ve směru a ve směru
samostatně. Po směru hodinových ručiček jsou to postupně dvojice
.
Move = tuple[int, int]
Dále budeme potřebovat převádět mezi indexem v seznamu Box a
souřadnicemi daného políčka. K tomu slouží následující dvě (čisté)
funkce.
def to_index(position: Position) -> int:
x, y = position
return y * 3 + x
Dále si zadefinujeme (opět čistou) funkci, která nám pro daný tah
vrátí ten opačný (když provedeme tah m a poté opposite(m),
nestane se nic – ujistěte se, že rozumíte, proč tomu tak je).
Základ herní mechaniky realizuje procedura move_blank, která
v daném rozložení kamenů posune prázdné místo ve směru daném
parametrem move. Pohyb realizuje výměnou hodnot na
odpovídajících pozicích v seznamu, který hru reprezentuje.
Dále nás bude zajímat, je-li nějaký tah při daném rozložení kamenů
přípustný, tzn. nepokusíme se přesunout neexistující kámen
(umístěný mimo hrací plochu) do prázdného místa, které je zrovna
na některém kraji. Tuto kontrolu realizuje predikát admissible.
Předposlední pomocná čistá funkce je distance, která nám řekne,
kolikrát se daný kámen musí určitě posunout, aby se dostal na své
správné místo. Protože kameny lze posouvat pouze v pravých úhlech,
záleží pouze na počtu horizontálních a počtu vertikálních posunů
samostatně. Uvažme například posuv ze souřadnic na
souřadnice (šipky reprezentují směr pohybu). Je vidět, že
určitě potřebujeme aspoň tři posuvy, co odpovídá naznačenému
vzorci – v našem příkladě tedy . Přesun lze jistě realizovat i více kroky,
nás ale bude zajímat minimum.
Toto číslo odpovídá tzv. Manhattanské metrice16 (vzdálenosti) mezi
současnou a koncovou pozicí daného kamene.
Vyzbrojeni minimálním počtem kroků, které potřebujeme k přesunu
daného kamene na své místo, se pokusíme odhadnout, kolik nejméně
kroků potřebujeme k vyřešení celého rébusu. Tento odhad je
naštěstí velmi jednoduchý: stačí si uvědomit, že přesunem jednoho
kamene se ke své koncové pozici přiblíží pouze tento kámen a
žádný jiný. Jistě se nám často stane, že kroků bude potřeba víc:
to nám ale nebude vadit, důležité je pouze to, abychom měli dobrý
spodní odhad.
def need_steps(box: Box) -> int:
total = 0
for tile in range(1, 9):
total += distance(box, tile)
return total
Tím jsou pomocné funkce vyřešeny a můžeme se pustit do samotného
hledání nejkratšího řešení. Stejně jako v předchozích ukázkách,
budeme používat rekurzi a backtracking, ale objeví se zde i
slibované novinky.
Dosud jsme všechny prohledávací algoritmy realizovali jako
čisté funkce. Prohledávací algoritmus pro „8 puzzle“ má ale
sdílený stav: efektivní řešení tohoto rébusu vyžaduje,
abychom sdíleli informace mezi jednotlivými podvýpočty. To nám
umožní ty, o kterých z předchozího prohledávání víme, že
nevedou k cíli, rychle zamítnout.
Protože beztak je výpočet realizován procedurou, nebudeme pro
každý tah vytvářet novou (upravenou) kopii stavu hry: místo
toho si budeme pamatovat pouze sekvenci tahů jako
explicitní zásobník a hrací plochu budeme upravovat in situ
(na místě). Ušetříme tak značné množství práce.
Sdílený stav zapouzdříme do třídy, která bude mít následovné
atributy:
best: délka dosud nalezeného nejlepšího řešení (k vyřešení
hry s nejdelším optimálním řešením je potřeba 31 tahů17 – toto
číslo tedy použijeme jako počáteční horní odhad pro délku),
found: nastavíme na True jakmile nalezneme libovolné
řešení,
moves: zmiňovaný zásobník tahů, které jsme provedli
z počáteční konfigurace, a který nám umožní efektivně se ve
výpočtu vracet,
box: aktuálně zkoumaná herní pozice,
visited: slovník,18 ve kterém si budeme pamatovat již objevené
herní pozice (konfigurace hrací plochy) a v kolika krocích
jsme k nim z té počáteční došli (tento slovník nám umožní
přeskočit velkou část redundantních podstromů).
Následující dvě metody realizují provedení jednoho tahu
(apply) resp. jeho vrácení (backtrack). Všimněte si, že
jsou to jediné dvě metody, které přímo modifikují jak aktuální
hrací pole, tak zásobník tahů.
Samotné rekurzivní hledání realizuje metoda-procedura
search.
def search(self) -> None:
Struktura rekurzivního řešení je stále zachována. Nejprve
jednoduché (přímo řešitelné nebo nezajímavé) případy. Ten
první jednoduchý případ je ale nového typu: nacházíme-li
se v konfiguraci, kterou jsme již někdy v minulosti
(v jiném podstromě) navštívili, zjistíme, kolik kroků jsme
na to v minulosti potřebovali (jak hluboko ve stromě se
nacházela).
Podstrom, který je na dané konfiguraci „zavěšen“ je totiž
vždy stejný: má smysl jej prohledávat pouze v případě, že
jsme tuto konfiguraci ještě nikdy nepotkali, nebo ji
potkali pouze ve větší hloubce. V tom druhém případě si
totiž celkovou délku cesty k řešení zkrátíme. Uvažme
například tuto situaci ( je počáteční konfigurace,
je současná konfigurace, která se ve stromě opakuje,
je vyřešený rébus):
Je vidět, že navštívíme-li uzel jako první, má smysl
uzel prohledat, protože cesta z do je
kratší, než cesta z do kterou jsme již našli.
Naopak, dostaneme-li se do uzlu poté, co jsme již
navštívili, nemůžeme touto cestou žádné lepší řešení
než nalézt a tento podstrom můžeme celý zamítnout.
Není-li konfigurace rovnou zamítnuta, nezapomeneme si pro
pozdější výpočet poznačit její hloubku do atributu
self.visited.
key = tuple(self.box)
if key in self.visited:
if self.visited[key] <= len(self.moves):
return
self.visited[key] = len(self.moves)
Druhý jednoduchý případ je již dobře známého typu: nalezli
jsme řešení. Zároveň si poznačíme jeho hloubku v případě,
že se jedná o řešení zatím nejlepší (nejkratší).
Poslední jednoduchý případ je ten, kdy již víme, že
nejkratší možná cesta ze současného stavu k řešení je
delší, než ta zatím nejlepší nalezená. K tomu s výhodou
použijeme pomocnou funkci need_steps, kterou jsme si
dříve definovali. Připomeňme si, že tato nám dává spodní
odhad na délku cesty k řešení: je-li tento příliš dlouhý,
skutečná délka bude jistě také.
if len(self.moves) + need_steps(self.box) > self.best:
return
Zbývá vyřešit případy, o kterých nelze přímo říct nic.
Rekurzivně tedy prohledáme podstromy, do kterých vedou
jednotlivé přípustné tahy. Najdeme-li v některé větvi nové
nejlepší řešení, rekurzivní volání tuto skutečnost poznačí
do atributů best a found.
for move in [(1, 0), (0, 1), (-1, 0), (0, -1)]:
if admissible(self.box, move):
self.apply(move)
self.search()
self.backtrack()
Pomocná metoda-procedura, která spustí hledání, a vrátí jeho
celkový výsledek: None v případě, kdy řešení neexistuje,
jinak délku toho nejlepšího možného.
def solve(self) -> int | None:
self.search()
return self.best if self.found else None
Hotové řešení jako obvykle otestujeme na několika příkladech.
V mnoha případech existuje víc než jedno nejkratší řešení, to na náš úkol ale nemá zásadní vliv, protože nás zajímá pouze jejich délka, kterou mají samozřejmě všechny společnou.
Počet tahů není vůbec jednoduché odvodit teoreticky. Horní mez 31 tahů byla určena výpočetně, vyhledáním optimálního řešení z každého přípustného herního stavu. Pro hru „15 puzzle“ je tato mez 80 tahů (opět získána výpočetně). Znalost dobrého horního odhadu na délku řešení je pro efektivitu našeho algoritmu klíčová: pro zobecnění hry na políček, kdy podobně dobrý odhad nemáme, je potřeba použít mírně sofistikovanější algoritmus. Jeho základní myšlenkou je nějakou mez zvolit, a nenajdeme-li v této mezi žádné řešení, postupně ji zvyšovat. To, jestli nějaké řešení existuje lze zjistit snadno z počáteční konfigurace, bez prohledávání.
Tento slovník má trochu zvláštní typ. Je to proto, že seznam nelze použít jako klíč: seznam (typ Box) tedy musíme převést na -tici, kterou již můžeme použít jako klíč. Zápis s třemi tečkami říká, že -tice obsahuje nějaký počet celých čísel, který není blíže určený.
Typ pro libovolně zanořený seznam znáte z přednášky:
NestedList = list['int | NestedList']
Vaším úkolem je napsat čistou funkci, která na vstupu dostane
NestedList (vnořený seznam celých čísel) a vrátí obyčejný
seznam, který zachovává pořadí čísel na vstupu, ale „zapomene“
strukturu vnoření.
Napište predikát, který rozhodne, zda lze dané číslo num napsat
jako součet , kde je zadáno parametrem count a
jsou po dvou různá kladná čísla. Jinými slovy, lze num
zapsat jako součet count druhých mocnin různých kladných čísel?
Napište čistou funkci, která ze vstupního seznamu vytvoří seznam
všech jeho permutací (tedy seznamů takových, že jsou tvořena
stejnými hodnotami v libovolném pořadí). Výsledný seznam permutací
nechť je uspořádán lexikograficky.
Nápověda: řešení se znatelně zjednoduší, budete-li celou dobu
pracovat se seřazenou verzí vstupního seznamu (seřazení je nakonec
také jen permutace). Dobré řešení pak vytvoří každou permutaci
pouze jednou a také je vytvoří rovnou ve správném pořadí.
Napište predikát, který dostane na vstupu množinu čísel a
délku a rozhodne, existuje-li navazující posloupnost čísel
délky právě . Navazující posloupnost je taková, kde každé další
číslo začíná v jedenáctkovém zápisu stejnou číslicí, jakou končí
předchozí. Čísla se v posloupnosti nesmí opakovat.
Napište čistou funkci, která vrátí množinu všech čísel, kterých
ciferný součet v desítkové soustavě je právě digit_sum a zároveň
jejich počet cifer není větší než max_length (rozmyslete si, že
bez tohoto omezení by byla hledaná množina nekonečná).
Vaším úkolem bude napsat čistou funkci, která vygeneruje všechny
rozklady dané množiny celých čísel. Pro zjednodušení nebudeme
pracovat s datovým typem množina, ale všechny množiny budeme
reprezentovat pomocí seznamů. Můžete předpokládat, že jednotlivé
prvky vstupního seznamu jsou unikátní.
Napište (čistou) funkci, která dostane na vstupu množinu čísel a
vrátí délku nejdelšího šestnáctkového kruhu, který se z nich dá
vytvořit. Pokud se žádný kruh vytvořit nedá, vrátí 0.
Šestnáctkový kruh je posloupnost čísel (bez opakování) taková, že
každé další číslo začíná v šestnáctkovém zápisu stejnou cifrou,
jakou končí číslo předchozí. Navíc první číslo v posloupnosti
začíná stejnou číslicí, jakou končí poslední číslo.
Z předmětu IB000 Matematické základy informatiky víme, že každá
relace ekvivalence na nějaké množině M jednoznačně určuje
rozklad množiny M, tedy množinu vzájemně disjunktních podmnožin,
jejichž sjednocením je celá množina M.
Platí to i naopak, každý rozklad jednoznačně určuje relaci
ekvivalence.
Například na množině M = {1, 2, 3} můžeme definovat relaci
ekvivalence {(1, 1), (2, 2), (2, 3), (3, 2), (3, 3)}, které
odpovídá rozklad [{1}, {2, 3}]
Napište funkci partition2pairs, která jako parametr dostane
rozklad množiny (tedy seznam podmnožin) a vrátí množinu
uspořádaných dvojic, které představují odpovídající relaci
ekvivalence.
Dále napište funkci pairs2partitions, která z relace zadané
jako množina uspořádaných dvojic vytvoří odpovídajíí rozklad
(seznam podmnožin).
V obou případech můžete předpokldádat, že vstup je korektní.
† Z přednášky již znáte vnořený seznam čísel NestedList:
NestedList = list['int | NestedList']
Napište proceduru, která na vstupu dostane NestedList celých
čísel a upraví ho tak, aby v něm byla čísla seřazená vzestupně
napříč všemi vnitřními seznamy. Například seznam [[4, 7, 1], [],
[8], [0, 5]] se použitím této procedury změní na [[0, 1, 4], [],
[5], [7, 8]].
Napište čistou funkci, která najde libovolnou podmnožinu zadané množiny
kladných celých čísel nums, součet jejíchž prvků je přesně
total. Pokud taková podmnožina neexistuje, funkce vrátí None.
Při řešení přemýšlejte, jestli některé výpočty neprovádíte opakovaně
a jak byste se tomu mohli vyhnout.
Ve třetí ukázce této kapitoly jsme řešili problém splnitelnosti
výrokové formule. Tato formule byla ve speciálním tvaru, takzvané
konjunktivní normální formě.
Nyní se podíváme na stejný problém pro formule v jiném speciálním
tvaru – v tzv. disjunktivní normální formě. V tomto tvaru se
formule skládá opět z klauzulí, tentokrát je ale jejich disjunkcí.
Uvnitř závorek se pak objevuje konjunkce literálů. Například:
Napište čistou funkci satisfiable, která rozhodne, je-li takto
zadaná formule splnitelná. Než se pustíte do řešení, dobře si
rozmyslete, co splnitelnost znamená a v jakých přesně případech je
formule v tomto tvaru (ne)splnitelná. Typy, kterými formuli
reprezentujeme jsou stejné, jako ty v ukázce.
Napište čistou funkci sum_different_powers, která pro zadané kladné celé
číslo num a celé číslo k ≥ 2 rozhodne, zda se dá num napsat jako součet
druhé, třetí, ... až kté mocniny různých kladných celých čísel.
Funkce musí rozumně fungovat pro num v řádech milionů a pro k do 10.
Příklad:
Volání {fun}(17, 3) vrátí True, protože .
Volání {fun}(80, 3) vrátí False, protože není žádný způsob, jak číslo
80 zapsat jako součet druhé a třetí mocniny různých kladných celých čísel.
Volání {fun}(365, 5) vrátí True, protože .
Volání {fun}(1000, 4) vrátí True, protože .
Volání {fun}(1002, 4) vrátí False, protože 1002 se nedá zapsat jako
součet druhé, třetí a čtvrté mocniny různých kladných celých čísel.
V tomto příkladu máme na vstupu neprázdný řetězec desítkových číslic
(tj. znaků '0' až '9'), který nezačíná znakem '0', a chceme je
rozsekat na části tak, aby tvořily rostoucí posloupnost čísel zapsaných
v desítkové soustavě, přitom žádná část nesmí začínat znakem '0'.
Ze všech takových posloupností pak chceme vybrat tu, která má co nejnižší
své poslední číslo. Vaším úkolem je napsat čistou funkci, která spočítá
toto číslo. Funkce by měla fungovat na vstupech o řádově desítkách znaků.
Příklad: Řetězec "23245" můžeme rozsekat na rostoucí posloupnosti
následujícími způsoby: 2, 3, 245 nebo 2, 32, 45 nebo 23, 245 nebo 23245.
Nejnižší poslední číslo je 45; volání
lowest_increasing_sequence_end("23245") tedy vrátí 45.
Tato kapitola přidává operace práci s řetězci. Krom nových
výrazů se drobná rozšíření dotknou i příkazu for (který můžeme
použít k procházení řetězce po znacích). Na rozdíl od seznamů ale
pro řetězce neexistuje vnitřní přiřazení.
Tato kapitola přináší také prostředky pro jednoduchou práci se
soubory a další interakci s prostředím (zejména operačním systémem).
Podobně jako tomu bylo v případě seznamů a n-tic, řetězce můžeme do
programu zapsat pomocí řetězcových literálů. Ty mají jeden
z těchto tvarů: 'znaky', "znaky", """znaky""", '''znaky'''.
Významově jsou všechny tyto tvary ekvivalentní: vytvoří hodnotu typu
řetězec, která obsahuje znaky.
Pro většinu znaků je obsah vzniklého řetězce totožný se zápisem
literálu, až na dva druhy výjimek:
některé znaky nebo sekvence znaků se v literálech nesmí mimo
speciální sekvence objevit:
znak konce řádku v literálech s jednoduchým oddělovačem
('znaky' a "znaky"),
samotný oddělovač (', ", ''', """) použitý pro zápis
daného literálu – nebylo by zřejmé, zda se jedná o konec
literálu nebo nikoliv,
některé sekvence znaků, které začínají znakem \ (zpětné
lomítko) se přeloží na jeden znak:
\', \" se přeloží na samotné znaky ' a ",
\\ se přeloží na znak \,
\n se přeloží na znak konce řádku,
\a, \b, \f, \r, \t, \v se přeloží na různé
speciální znaky, které v tomto kurzu nebudou důležité,
\NNN a \xNN, \uNNNN, \UNNNNNNNN, kde N… je tříciferný
osmičkový nebo dvou-, čtyř- nebo osmiciferný šestnáctkový zápis
nějakého čísla , se přeloží na znak x který má v tabulce
znaků Unicode pozici .
Snadno se přesvědčíte, že „zakázané“ znaky resp. sekvence znaků lze
vždy zapsat nějakým alternativním způsobem pomocí \-sekvencí.
Podobně jako seznamy, řetězce lze indexovat: zápis je stejný jako
u seznamů: řetězec[index], kde řetězec je jméno a index je
celočíselný výraz. Na rozdíl od seznamů, výsledkem indexace je
v případě řetězce opět řetězec, který ale obsahuje pouze jediný
znak.
Dále nově připouštíme relační operátory x == y, x != y, x < y,
x > y, x <= y, x >= y i v případě, kdy se podvýrazy x a y
oba vyhodnotí na řetězce. Uspořádání je dáno lexikograficky.
kde ch je jméno a řetězec je výraz, který se vyhodnotí na
hodnotu typu řetězec. Podobně jako ostatní varianty příkazu for,
tento provede sekvenci příkazy jednou pro každý znak uložený
v řetězci řetězec. Jméno ch je přitom v -té iteraci vázáno na
jednopísmenný řetězec odpovídající znaku na -té pozici hodnoty
řetězec.
Pro práci se soubory (a dalšími zdroji, o kterých ale v tomto
předmětu nebude řeč) budeme krom zabudovaného podprogramu open
(vysvětleno níže) slouží také příkaz with – je obvyklé je používat
vždy společně, a to ve tvaru:
with open(cesta, režim) as název:
příkazy
Tato konstrukce nám umožní se souborem pracovat v těle příkazu
with pomocí jména název (stejně, jako kdybychom přiřadili
výsledek volání open do proměnné), ale navíc máme zaručeno, že po
opuštění tohoto bloku je práce se souborem korektně ukončena.
Takto otevřený a pojmenovaný soubor můžeme iterovat již dobře
známým příkazem for:
for řádek in soubor:
příkazy
kde řádek je jméno a soubor je výsledek volání open (obvykle
vázaný příkazem with). Ke jménu řádek budou postupně vázány
hodnoty typu str, které obsahují vždy jeden řádek souboru (včetně
ukončovacího znaku '\n'). Cyklus je ukončen po přečtení posledního
řádku.
Objekty typu řetězec navíc poskytují tyto zabudované metody (ve
všech případech jsou zároveň čistými funkcemi – vstupní řetězec
nikdy nemodifikují):
s.isupper(), s.islower() – predikáty, vyhodnotí se na True
v případě, že všechny abecední znaky v řetězci s jsou velká
(resp. malá) písmena,
s.isalpha(), s.isdecimal() – predikáty, které se vyhodnotí na
True sestává-li s pouze z abecedních znaků (isalpha) resp.
desítkových číslic (isdecimal),
s.upper(), s.lower() – vyhodnotí se na řetězec, který vznikne
ze s nahrazením všech abecedních znaků na odpovídající velká
(upper) resp. malá (lower) písmena,
s.split(delim) – vyhodnotí se na seznam, který vznikne
rozdělením s na podřetězce oddělovačem delim (oddělovače
nejsou součástí výsledných řetězců),
s.join(parts) – vyhodnotí se na řetězec, který vznikne vložením
řetězce s mezi každé dva řetězce uložené v seznamuparts,
s.replace(from, to) – vyhodnotí se na řetězec, který vznikne
ze s substitucí všech výskytů podřetězce from za podřetězec
to,
s.rstrip() – vyhodnotí se na řetězec, který vznikne odstraněním
všech pravostranných bílých znaků (zejména mezer a znaků konce
řádku).
Jak bylo naznačeno výše, práci se soubory nám umožňuje zabudovaný
podprogram open(cesta, režim)19. Parametr cesta (typu řetězec)
určuje kde v souborovém systému se má hledat soubor, se kterým
chceme pracovat, řetězec režim pak určuje jakým způsobem hodláme
soubor používat. Základní možnosti jsou tyto:
'r' – režim pouze pro čtení nám umožní ze souboru číst textová
data, ale nic dalšího,
'w' – režim pro zápis textu, kdy je soubor při otevření zkrácen
na nulovou délku (z takto otevřeného souboru nelze číst),
'x' – jako 'w', ale soubor je prvně vytvořen (v případě, že
již existuje, je program ukončen s chybou),
'a' – jako 'w', ale soubor není zkrácen, nová data jsou
zapisována na konec souboru.
Tyto základní možnosti lze kombinovat se specifikátorem 't' nebo
'b', který určí, chceme-li se souborem pracovat v textovém nebo
binárním režimu. Neuvedeme-li ani jedno z nich, implicitní je
textový režim. V tomto předmětu se omezíme na textový režim.
S hodnotou f, které vznikne voláním podprogramu open v textovém
režimu, můžeme použít také několik zabudovaných metod:
f.close() – ukončí práci se souborem (obvykle nepoužíváme,
ukončení provedeme místo toho správným použitím příkazu with),
f.read(n) – přečte nejvýše n znaků a vrátí je jako hodnotu
typu str,
f.readline() – přečte znaky od aktuální pozice až do konce
řádku a vrátí je jako hodnotu typu str,
f.readlines() – přečte celý zbytek souboru po řádcích,
výsledkem je hodnota typu list, která obsahuje pro každý
přečtený řádek jednu položku typu str,
f.write(s) – zapíše řetězec s (t.j. hodnotu typu str) do
souboru.
Většina funkcionality pro interakci s vnějším světem je k dispozici
formou knihoven (obdoba knihovny math, kterou známe z první
kapitoly). Zde uvádíme pouze stručný přehled, bližší informace
k použití jednotlivých knihoven získáte v 11. přednášce. Použití
knihovny je potřeba vždy na začátku souboru deklarovat řádkem
from knihovna import jméno₁, jméno₂, …
K dispozici máme tyto knihovny:
gzip – práce s komprimovanými soubory *.gz,
open – otevře komprimovaný soubor (dále s ním lze pracovat
jako s obyčejným souborem, liší se ale implicitním použitím
binárního režimu) – voláme pomocí příkazu with,
csv – práce s textovými soubory, které obsahují tabulky hodnot
oddělené čárkou (nebo jiným oddělovačem),
sys – obecná interakce se systémem:
argv – seznam hodnot typu str, které byly programu předány
při spuštění na příkazové řádce,
os – další podprogramy (zejména procedury) pro práci se
systémem (cesta je hodnota typu str):
V této ukázce načteme seznam slov uložených v komprimovaném
souboru (ve formátu gzip) a použijeme jej k implementaci (velmi
zjednodušené) kontroly pravopisu. K načtení souboru použijeme
standardní modul gzip.
import gzip
Načtení slovníku realizujeme jednoduchým podprogramem
read_dictionary, který soubor dekomprimuje a slova uloží do
množiny (množina proto, abychom dokázali slova rychle vyhledávat).
Výstup dekompresního algoritmu budeme číst (písmenko r
v parametru mode) v textovém režimu (písmenko t).
Dekomprimovaná data pak již čteme stejně jako libovolný jiný
soubor, třeba iterací, která postupně vrací jednotlivé řádky.
Protože slova jsou v souboru uložena ve formátu 1 řádek = 1 slovo,
bude nám právě tento režim vyhovovat. K odstranění znaků konce
řádku použijeme metodu strip.
def read_dictionary(path: str) -> set[str]:
out: set[str] = set()
with gzip.open(path, 'rt') as data:
for word in data:
out.add(word.strip())
return out
Samotnou kontrolu provede čistá funkce spellcheck. Vstupem je
množina přípustných slov (obsah seznamu slov načteného výše) a
text, který chceme zkontrolovat. Výstupem je pak krom samotných
neznámých slov také seznam čísel řádků, na kterých se ve vstupu
objevují. K reprezentaci použijeme slovník, kde klíčem je špatně
napsané slovo a hodnotou zmiňovaný seznam.
Abychom se alespoň trochu přiblížili realitě, budeme se chtít
vypořádat s některými problémy:
slova nejsou vždy oddělena mezerami: často se objevují čárky,
tečky, uvozovky, závorky a podobně,
na velikosti písmen občas záleží, ale ne vždy:
slovo, které ve slovníku obsahuje velká písmena, je,
napíšeme-li jej malými písmeny, typicky chybou (třeba
„Jean-Pierre“)
naopak, slovo, které je ve slovníku malými písmeny, může
v textu stát na začátku věty, nebo obsahovat velká písmena
z jiného důvodu, a typicky to chyba není.
Skutečné programy pro kontrolu pravopisu jsou obvykle mnohem
složitější, nám ale bude tato úroveň realizmu stačit. Metody,
které neznáte, si dohledejte v dokumentaci: i to je důležitá
součást programování.
def spellcheck(dictionary: set[str], text: str) -> dict[str, list[int]]:
problems: dict[str, list[int]] = {}
to_erase = {',', '.', '!', '?', '(', ')', '"'}
for lineno, line in enumerate(text.split('\n')):
processed = ''
for char in line:
processed += ' ' if char in to_erase else char
for word in processed.split():
if word not in dictionary and \
word.lower() not in dictionary:
if word not in problems:
problems[word] = []
problems[word].append(lineno + 1)
return problems
Celý program otestujeme na několika jednoduchých vstupech. Slovník
naleznete v souboru zz.words.gz (na stroji aisa si jej můžete
prohlédnout třeba příkazem zless).
V této ukázce se zaměříme na rekurzivní procedury pro práci
s výstupem. Konkrétně se budeme zabývat vnořenými odrážkovými
seznamy, které budeme v programu reprezentovat jako seznam objektů
typu Item. Každá odrážka (instance Item) v takovém seznamu má
nějaký vlastní text (atribut text) a případně seznam pododrážek
(atribut sublists).
class Item:
def __init__(self, text: str):
self.text: str = text
self.sublists: list[Item] = []
V parametru itemize budeme proceduře print_itemize_rec
předávat relevantní odrážkový seznam, v parametru prefix budeme
uchovávat řetězec, který vypíšeme před každou jednotlivou
odrážkou: tím budeme realizovat zanoření, které by mělo ve výstupu
vypadat takto:
- odrážka 1
- odrážka druhé úrovně
- další odrážka druhé úrovně
- odrážka 2
- zanořená odrážka
- ještě zanořenějši odrážka
Na této proceduře je zajímavé také to, že bázový případ není
zmíněn explicitně: pozorný čtenář si ale jistě všimne, že odrážka,
která již žádné pododrážky nemá, bude mít seznam sublists
prázdný. Na prázdném seznamu ale procedura print_itemize_rec
neudělá vůbec nic: cyklus v jejím těle se ani jednou neprovede.
Výstup postupně sestavujeme v seznamu lines, který si předáváme
pomocným parametrem.
def format_itemize(itemize: list[Item], prefix: str,
lines: list[str]) -> None:
for i in itemize:
lines.append(prefix + '- ' + i.text + "\n")
format_itemize(i.sublists, prefix + ' ', lines)
Procedura print_itemize pomocí procedury format_itemize
vytvoří seznam řádků a tyto uloží do souboru: krom otevření
souboru pro zápis se stará také o nastartování rekurze.
def print_itemize(itemize: list[Item], path: str) -> None:
lines: list[str] = []
format_itemize(itemize, '', lines)
with open(path, 'w') as out:
for line in lines:
out.write(line)
Tím je ukázka kompletní. Program jako obvykle otestujeme na
jednoduchém vstupu.
Tato ukázka je variací na předchozí: budeme opět zapisovat
rekurzivní datovou strukturu do souboru, tentokrát na to ale
použijeme zápis bez rekurze. Nejprve si zadefinujeme potřebné
typy, zejména třídu NestedDict. Tato reprezentuje zanořený
slovník, kde klíče jsou řetězce a hodnoty jsou buď řetězce, nebo
vnořené slovníky.
NestedDict = dict[str, 'str | NestedDict']
Výpis slovníku provede procedura print_nested. Formát výpisu
bude následovný:
je-li ke klíči asociovaná hodnota typu řetězec, klíč a hodnota
se vypíšou na jeden řádek, oddělené dvojtečkou, patřičně
odsazené dle úrovně zanoření,
je-li hodnota zanořený slovník, klíč se vypíše na samostatný
řádek ukončený dvojtečkou a obsah slovníku se vypíše pod něj,
odsazený o jednu mezeru navíc.
Klíče seznamu budou seřazeny abecedně. Příklad:
klíč 1:
abecedně první klíč vnořeného slovníku: řetězec
další klíč vnořeného slovníku: jiný řetězec
třetí klíč:
více zanořený klíč: další řetězec
klíč 2: řetězec v hlavním slovníku
Z kapitoly 6 si jistě pamatujete základní datové struktury:
k procházení rekurzivní struktury bez použití rekurze se bude
hodit zásobník, který budeme realizovat seznamem a jeho metodami
append (vloží prvek na vrchol zásobníku) a pop (odebere prvek
z vrcholu).
Začneme tím, že si otevřeme soubor path pro zápis a výsledek
si poznačíme do proměnné out.
with open(path, 'w') as out:
Dále si nachystáme zásobník, ve kterém budeme uchovávat
rozpracované podúlohy. Tyto budeme reprezentovat jako
dvojice:
jednak si musíme pamatovat, který zanořený slovník na
dané úrovni zanoření právě zpracováváme (toto bude
první složka),
dále pak u každého rozpracovaného slovníku potřebujeme
vědět, které klíče je ještě potřeba zpracovat (resp.
které jsme již vypsali).
Pro začátek na zásobník vložíme „hlavní“ slovník (ten,
který jsme dostali jako parametr) a poznačíme si, že
musíme zpracovat všechny jeho klíče. Protože klíče ke
zpracování budeme odebírat z konce seznamu (kvůli
efektivitě), vložíme je do seznamu v opačném abecedním
pořadí.
stack = []
todo = list(records.keys())
todo.sort()
todo.reverse()
stack.append((records, todo))
Tím máme nachystaný počáteční stav a dále budeme
zpracovávat jednotlivé podúlohy, a každou, kterou
dokončíme ze zásobníku odstraníme. Podúlohy budeme
zpracovávat až do chvíle, kdy se zásobník zcela vyprázdní.
Narazíme-li během zpracování některé podúlohy na další
(vnořený slovník), podobně je vložíme do zásobníku.
while stack:
Pracujeme vždy s podúlohou na vrcholu zásobníku, tzn.
tou „nejnovější“ (vzpomeňte si, že zásobník je „last
in, first out“).
items, keys = stack[-1]
Dojdou-li nám v daném slovníku (podúloze) klíče ke
zpracování, jsme hotovi: podúlohu odstraníme ze
zásobníku a pokračujeme ve výpočtu s další podúlohou
(která se tímto dostala na vrchol).
if not keys:
stack.pop()
continue
Množina klíčů ke zpracování nebyla prázdná – stojíme
tedy před nedokončenou podúlohou. Ze seznamu
nezpracovaných klíčů jeden vybereme a zpracujeme
(k tomu budeme potřebovat i odpovídající hodnotu).
key = keys.pop()
value = items[key]
Pro účely výpisu si spočteme řetězec s mezerami, které
je potřeba umístit na začátek řádku – protože „hlavní“
slovník je odsazen o 0 mezer, počet mezer je o jedna
menší než současná hloubka zásobníku.
prefix = ''.join([' ' for _ in range(len(stack) - 1)])
Nyní se musíme rozhodnout, jakého typu je hodnota,
kterou máme zpracovat: je-li to řetězec, vypíšeme jej
přímo ke klíči. Naopak, je-li to zanořený slovník,
vypíšeme pouze klíč a podslovník zařadíme mezi
podúkoly, které je potřeba zpracovat, a to tak, že jej
(opět se všemi klíči) vložíme na vrchol zásobníku.
Napište funkci, která ve vstupním souboru najde 3 nejčastější
slova. Obsahuje-li soubor méně než 3 různá slova, výsledný seznam
bude kratší. V případě, kdy mají dvě slova stejnou frekvenci
výskytu, upřednostněte to, které je lexikograficky menší.
Napište proceduru write_config, která do souboru zadaného cestou
filename zapíše konfiguraci ze slovníku config. (Pokud už
takový soubor existuje, přepište jej.) Struktura slovníku je
taková, že klíč je název sekce a hodnotou další slovník, který již
obsahuje dvojice klíč-hodnota typu řetězec.
Formát výstupního souboru nechť je následující:
prázdné sekce (takové, kterým je přiřazený prázdný slovník)
ignorujeme,
pro každou neprázdnou sekci zapíšeme řádek [jméno sekce] a na
další řádky postupně vypíšeme obsah příslušného slovníku ve
formátu klíč = "hodnota".
sekce i jednotlivé klíče v každé sekci uspořádejte na výstupu
podle abecedy.
Napište predikát, jehož hodnota bude True pokud lze požadované slovo
wanted utvořit z iniciálního slova initial pomocí přepisovacích pravidel
rules a False jinak. Slova vytváříme tak, že kterékoli písmeno z již
vytvořených slov nacházející se mezi klíči slovníku pravidel rules
můžeme nahradit za kterékoli písmeno z příslušné hodnoty. (Pro zjednodušení
možnost zacyklení procesu vytváření slov nemusíte vůbec řešit.)
V této úloze se budeme zabývat adresami protokolu IP verze 4,
které sestávají ze 4 čísel oddělených tečkami, například
192.0.2.0 (více informací o IPv4 naleznete například na
Wikipedii). Adresy budeme reprezentovat řetězci.
Napište predikát, kterého hodnota bude True, představuje-li jeho
parametr validní IPv4 adresu. Daná IPv4 adresa je validní právě
tehdy, když je tvořená čtyřmi dekadickými čísly od 0 až 255
(včetně) oddělenými tečkou (pro jednoduchost v této úloze
připouštíme pouze kanonický tvar IPv4 adres).
def ipv4_validate(address):
pass
Dále napište čistou funkci, která vypočte číselnou hodnotu dané
adresy. Konverze IPv4 adresy na její číselnou hodnotu je podobná
konverzi binárního zápisu čísla na dekadický s tím rozdílem, že
u IPv4 adresy pracujeme se základem 256. Hodnota adresy
192.0.2.0 je tedy . Můžete počítat s tím, že vstupem bude vždy validní
IPv4 adresa ve výše popsaném kanonickém tvaru.
Seznam budeme na výstupu reprezentovat dvěma třídami:
Item reprezentuje odrážku s textem v atributu text a
případným podseznamem v atributu sublists,
Itemize pak reprezentuje seznam jako celek, se jménem
name a odrážkami první úrovně v seznamu items.
Tyto třídy nijak nemodifikujte.
class Item:
def __init__(self, text: str):
self.text: str = text
self.sublists: list[Item] = []
class Itemize:
def __init__(self, name: str):
self.name: str = name
self.items: list[Item] = []
Implementujte podprogram parse_lists, který vrátí seznam
instancí třídy Itemize, které přečte ze souboru s názvem
filename. Můžete předpokládat, že soubor obsahuje pouze správně
formátované seznamy a mezi každými dvěma seznamy je jeden prázdný
řádek.
† V tomto příkladu budeme pracovat s n-árními stromy, které nemají
v uzlech žádné hodnoty (mají pouze stromovou strukturu).
Třídu Tree nijak nemodifikujte.
class Tree:
def __init__(self) -> None:
self.children: list[Tree] = []
Napište (čistou) funkci, které na základě dobře uzávorkovaného
řetězce tvořeného pouze znaky ( a ) vybuduje instanci výše
popsaného stromu, a to tak, že každý pár závorek reprezentuje
jeden uzel, a jejich obsah reprezentuje podstrom, který v tomto
uzlu začíná. Ve vstupním řetězci bude vždy alespoň jeden pár
závorek.
Napište čistou funkci, která na základě daného vzoru vytvoří
množinu všech odpovídajících řetězců. Vzor je tvořený
alfanumerickými znaky a navíc může obsahovat hranaté závorky –
znaky [ a ]. Mezi těmito závorkami může stát libovolný počet
přípustných znaků (krom samotných hranatých závorek) a na daném
místě se ve výsledném řetězci může nacházet libovolný z těchto
znaků. Například vzor a[bc]d reprezentuje řetězce abd a acd.
V tomto příkladu budeme pracovat se stromy, které mají
v jednotlivých uzlech uloženy řetězce. Tyto stromy budeme používat
k reprezentaci aritmetických výrazů složených z konstant a
binárních operátorů:
konstantu reprezentuje strom, který má oba podstromy prázdné,
složený výraz je reprezentován stromem, který má v kořenu
uložen operátor a jeho neprázdné podstromy reprezentují
operandy.
Žádné jiné uzly ve stromě přítomny nebudou.
class Tree:
def __init__(self, value: str,
left: 'Tree | None',
right: 'Tree | None'):
self.value = value
self.left = left
self.right = right
Napište čistou funkci, která dostane výše popsaný strom jako
parametr a vrátí odpovídající plně uzávorkovaný aritmetický výraz,
formou řetězce. Plným uzávorkováním myslíme, že každému
aritmetickému operátoru přísluší jedna dvojice kulatých závorek.
Napište (čistou) funkci, která dostane na vstup řetězec složený
pouze z číslic od 1 do 9 včetně a vrátí množinu všech možných IPv4
adres, z nichž tento řetězec mohl vzniknout vynecháním teček.
Za IPv4 adresu považujeme řetězec tvořený čtyřmi čísly v rozsahu
od 0 po 255 včetně oddělenými tečkami. Například řetězec
25525511135 mohl vzniknout výše popsaným způsobem z adres
255.255.11.135 a 255.255.111.35.
Někdy se stane, že při programování v Pythonu omylem necháte na
konci řádku mezery, nebo jiné bílé znaky (např. tabulátor). Při
kontrole programem edulint je toto označeno za chybu. Vaším
úkolem je napsat jednoduchý program, který tento typ chyby
v zadaných souborech opraví. Seznam souborů k opravě dostanete
jako argumenty na příkazové řádce (v Pythonu je naleznete
v seznamu sys.argv počínaje indexem 1). Soubor, se kterým právě
pracujete, můžete načíst celý do paměti.
Poznámka: tento program lze testovat dvěma způsoby. Spustíte-li
jej bez dalších parametrů, spustí se přiložené testy. Předáte-li
naopak programu nějaké parametry, spustí se přímo procedura
trailing, která tyto zpracuje obvyklým způsobem. Například:
V první ukázce jsme viděli jednoduchý program na kontrolu
pravopisu. Tento úkol bude podobný, ale místo vyznačení nalezených
chyb je budeme rovnou opravovat.
Ze 4. kapitoly si možná pamatujete tzv. Hammingovu vzdálenost:
jednalo se o funkci, která dvojici slov stejné délky přidělí
nezáporné celé číslo: počet znaků, ve kterých se liší. Náš
„autocorrect“ bude pro jednoduchost používat právě tuto metriku.
Pro každé slovo ze vstupu, které se nenachází ve slovníku, tedy:
nalezněte všechna slova stejné délky,
vyberte ta, která mají minimální Hammingovu vzdálenost od toho
vstupního,
obsahuje-li seznam slova, která se se vstupem shodují na první
pozici, ponechte pouze tato,
obdobně na poslední pozici, pak na druhé, předposlední, atd.,
ze zbytku vyberte první slovo dle abecedy a toto použijte jako
opravu.
Procedura autocorrect má 3 parametry: název souboru
s komprimovaným slovníkem (ve formátu gzip), název vstupního
souboru a název výstupního souboru, do kterého zapíše opravený
text. Níže máte nachystaných několik čistých funkcí, které Vám
řešení můžou usnadnit – rozmyslete si, co dělají, a jak je použít.
for curr_word in words:
distance = hamming(word, curr_word)
if best is None or distance < best:
res = set()
best = distance
if distance == best:
res.add(curr_word)
return res
def closest_by_ends(word: str, candidates: set[str]) -> set[str]:
for offset in range(len(word) // 2):
for direction in [-1, 1]:
idx = direction * offset
filtered = set()
for curr_word in candidates:
if word[idx] == curr_word[idx]:
filtered.add(curr_word)
Jednou z možností, jak poznat v jakém (přirozeném) jazyce je
nějaký dokument napsaný, je jednoduchá statistická analýza.
Napište funkci, která dostane jako parametr slovník lang_freq a
název souboru text_file:
lang_freq bude pro každý jazyk obsahovat slovník tvaru {
'a': 357907, 'b': 113756, … } kde hodnota u každého písmene
je počet jeho výskytů v nějakém reprezentativním dokumentu,
soubor text_file je textový soubor, kterého jazyk chceme
určit.
Jazyk určujte tak, že spočítáte frekvence jednotlivých písmen
v souboru text_file a srovnáte je s těmi uloženými ve slovníku
lang_freq.
Jak nalezneme nejlepší shodu? Informace o frekvenci písmen
v nějakém dokumentu lze chápat jako vektory v 26-rozměrném
prostoru (resp. vícerozměrném, uvažujeme-li písmena s diakritikou,
ale přesná dimenze není podstatná). Za nejpodobnější budeme
považovat vektory, které svírají nejmenší úhel. Tento získáte ze
vztahu (kde na levé straně je běžný
skalární součin, „absolutní hodnoty“ na straně pravé jsou pak
délky, které zjistíte ze vztahu ).
Napište čistou funkci, která vrátí množinu všech slov, tvořených
znaky {"0", "1", "2"} s danou délkou length a váhou weight.
Váhou myslíme počet nenulových číslic v daném slově.
V tomto příkladu budeme pracovat s textovými soubory, v nichž nás budou
zajímat kulaté, hranaté a složené závorky.
Napište funkci count_fully_enclosed, která v případě, že je obsah souboru
korektně uzávorkován, vrátí počet nezávorkových znaků, které jsou uzavřeny
do všech tří typů závorek. Znaky konce řádku přitom nepočítáme.
Není-li obsah souboru korektně uzávorkován, funkce vrátí None.
Příklad:
Je-li na vstupu soubor s tímto obsahem:
a + (((
b - c) + d)
[{{(x, y)}}])
(písmeno a stojí na začátku řádku),
pak má funkce vrátit číslo 4, protože jsou zde celkem čtyři nezávorkové
znaky, které jsou uzavřeny do všech tří typů závorek
(jsou to znaky x, y – za čárkou je mezera).
def count_fully_enclosed(filename: str) -> int | None:
pass
V tomto příkladu budeme pracovat s textovými soubory, které budou
obsahovat následující editační značky (dvouznakové; první znak je vždy
symbol stříšky ^):
^H znamená „smazat předchozí znak“ (pokud žádný předchozí znak
není, nestane se nic);
^U má význam podle toho, kde se nachází; pokud se nachází na začátku
řádku, znamená „vrátit se na konec předchozího řádku“, pokud se
nachází jinde, znamená „smazat vše od začátku řádku“;`
^W znamená „smazat předchozí slovo na tomto řádku“ (včetně
případných mezer, které stojí mezi posledním slovem a značkou
^W; pokud žádné předchozí slovo na aktuálním řádku není,
chová se jako ^U).
Slovo zde definujeme jako libovolnou posloupnost nemezerových znaků (tedy
např. řetězec "␣␣␣Hello,␣world!␣␣" obsahuje dvě slova – mezery zde
zdůrazňujeme znakem ␣). Smíte předpokládat, že se v souboru nevyskytují
jiné bílé znaky než mezery a konce řádků.
Napište funkci apply_edit_marks, která přečte soubor s editačními
značkami a vrátí řetězec, který vznikne tak, že se všechny úpravy
naznačené editačními značkami provedou. Úpravy se provádějí postupně od
prvního řádku a zleva doprava, tedy se např. značka ^U může dostat na
začátek řádku předchozími úpravami a pak se chová tak, jak se má chovat
na začátku řádku. Smíte předpokládat, že se symbol stříšky ^ v souboru
nevyskytuje jinde než ve výše uvedených značkách.
Příklad:
Je-li na vstupu soubor s tímto obsahem:
Hello, world^W^H^H!
How are you tonight?
^U^Wtoday?
Everything is
awesome^U good ^W^H^U okay, i^HI guess. ^Whope.
(první písmeno H stojí na začátku řádku),
pak funkce vrátí řetězec:
"Hello!\nHow are you today?\nEverything is okay, I hope.\n"
(\n zde reprezentuje znak konce řádku, jak je nejen v Pythonu obvyklé).
Představte si robota, který se umí pohybovat rovně dopředu o zadanou
celočíselnou délku, otáčet se o 90° v obou směrech a případně za sebou
nechávat stopu (tj. označovat místa, přes která jde).
Pozici robota reprezentujeme dvojicí celých čísel; první souřadnice je
x-ová (záporná čísla jsou na západ od počátku, kladná na východ), druhá
souřadnice je y-ová (záporná čísla jsou na sever, kladná na jih).
Na začátku je na souřadnicích (0, 0), je otočen k východu a je ve stavu,
že za sebou nezanechává stopu.
Funkce simulate_paintbot přečte ze zadaného souboru seznam instrukcí
pro robota a bude je vykonávat do chvíle, než robot při pohybu narazí na
vlastní stopu, tj. vejde na již označené místo.
Funkce vrátí robotovu poslední pozici (tedy tu, na které narazil na
vlastní stopu, nebo tu, kde skončil s vykonáváním poslední instrukce).
Předpokládejte, že zadaný textový soubor není prázdný a obsahuje
následující typy instrukcí (vždy jedna instrukce na řádku, žádné extra
mezery na začátku ani na konci řádku):
rotate left – robot se otočí o 90° doleva;
rotate right – robot se otočí o 90° doprava;
walk k – robot popojde o k jednotek dopředu, kde k je
právě jedna římská číslice (tabulka níže; pokud je robot ve
stavu, že za sebou zanechává stopu, tak označí všechna místa,
kterými projde, včetně toho posledního, kam došel);
toggle – pokud za sebou robot zanechával stopu, tak odteď nebude;
v opačném případě stopu zanechávat začne (počínaje aktuální pozicí).
Zde k může být jedno z:
I = 1 krok,
V = 5 kroků,
X = 10 kroků,
L = 50 kroků,
C = 100 kroků,
D = 500 kroků,
M = 1000 kroků.
Smíte předpokládat, že celkový počet polí, které robot v průběhu
vykonávání instrukcí projde, je menší než milion.
Toto je poslední kapitola hlavní části sbírky. Příklady této
kapitoly slouží k procvičení učiva z celého semestru, neobjevují se
zde již žádné nové koncepty ani konstrukce.
Do červí díry spadne seznam kladných celých čísel nums a množina
cifer (celá čísla od 0 po 9) allowed. Na druhém konci vypadnou
pouze ta čísla, jejichž všechny cifry jsou v množině allowed.
Napište čistou funkci wormhole, která vrátí seznam všech čísel
ze seznamu nums, která projdou červí dírou (pořadí zachovejte
podle vstupního seznamu).
Napište čistou funkci word_wrap která podle potřeby nahradí
mezery ve vstupním řetězci orig za znaky nového řádku, a to tak,
aby pro každý řádek platilo, že je buď dlouhý nejvýše
max_line_len znaků, nebo neobsahuje žádné mezery.
Dále napište čistou funkci without_middle_occurrence, která
dostane jako parametr seznam čísel values a hledané číslo
value a vrátí seznam bez prostředního výskytu hledaného čísla.
Vyskytuje-li se hledané číslo v zadaném seznamu sudý počet krát,
bereme jako prostřední ten blíže začátku, tedy např. pro vstup
([2, 2, 3, 2, 2], 2) funkce vrátí [2, 3, 2, 2]. (Pokud seznam
hledané číslo neobsahuje, vraťte původní seznam nebo jeho kopii.)
Napište funkci bowling_score, která spočítá celkové skóre bowlingové hry,
přičemž počty shozených kuželek jsou v seznamu rolls (předpokládejte, že
tento seznam obsahuje validní hody a že je dostatečně dlouhý). Skóre v
bowlingu se počítá takto: Hraje se na 10 kol, v každém kole se háže až
dvakrát, kromě posledního, kde se za určitých okolností háže třikrát. Pokud
hned prvním hodem kola dosáhne hráč 10 bodů (strike), podruhé už neháže a
do skóre se mu započítá 10 plus hodnoty dvou dalších hodů. Pokud v součtu
obou hodů dosáhne hráč 10 bodů (spare), do skóre se mu započítá 10 plus
hodnota jednoho dalšího hodu. V ostatních případech se do skóre započítá
součet obou hodů kola. Pokud hráč zahrál strike v posledním kole, háže ještě
dvakrát. Pokud hráč zahrál spare v posledním kole, háže ještě jednou.
Příklad: Pro vstup [10, 10, 3, 6, 4, 5, 9, 1, 7, 3, 10, 0, 1, 10, 3, 7, 10]
funkce vrátí 149; pro vstupní seznam obsahující dvanáctkrát 10 funkce
vrátí 300.
Vysvětlení prvního příkladu:
kolo: strike, počítá se bodů
kolo: strike, počítá se bodů
kolo: bodů
kolo: bodů
kolo: spare, počítá se bodů
kolo: spare, počítá se bodů
kolo: strike, počítá se bodů
kolo: bod
kolo: strike, počítá se bodů
kolo: spare, háže se tedy ještě jednou a počítá se
bodů. Celkem 149 bodů.
Rozdělení hodů do jednotlivých kol pro názornost:
Vysvětlení druhého příkladu:
V každém kole padne strike, počítá se tedy
bodů. V posledním kole rovněž padne strike, háže se tedy ještě dvakrát
a počítá se opět bodů. Dohromady tedy 10 kol po 30
bodech, což je 300 bodů. Rozdělení hodů do jednotlivých kol pro názornost:
Napište funkci count_seq, která nad desítkovou reprezentací
nezáporného celého čísla num provede následující výpočet:
vybere všechny cifry, po kterých následuje alespoň seq
stejných cifer; pro účely této kontroly chápeme num
cyklicky, tzn. po poslední cifře následuje opět první,
vybrané cifry sečte a součet vrátí.
Cykličnost v bodě 1 můžeme chápat jako nekonečné opakování num,
např. v čísle 123 následují po cifře 2 cifry 3, 1, 2, 3, 1, atd.
Příklady výpočtu:
pro num=111222 a seq=2 je výsledkem 3 (1 + 2), protože po
první (1) a čtvrté (2) cifře následují 2 stejné cifry,
pro num=1111 a seq=1 je výsledkem 4, protože po každé cifře
následuje alespoň jedna stejná cifra,
pro num=1234 a seq=0 je výsledkem součet všech číslic,
totiž 10.
Napište čistou funkci restore_sequence, která dostane neprázdný řetězec
složený pouze z číslic 0 a 1 a vrátí množinu všech možných
řetězců, které vzniknou doplněním znaků čárky ',' do původního
řetězce tak, aby části jimi oddělené byly dvojkové zápisy čísel
v intervalu od low do high včetně. Hodnota low bude vždy
alespoň 1. Rozdělení musí být takové, že žádný zápis neobsahuje
levostranné nuly.
Napište čistou funkci wordmask, která vypočte všechny možnosti
zamaskování slova word. Slovo zamaskujete aplikováním masky
mask, tj. na každý znak slova se aplikuje korespondující znak
masky. Je-li maska kratší než slovo, aplikuje se cyklicky.
Například pro slovo abababa a masku XX? je situace následovná
(odpovídající písmena jsou pod sebou):
abababa
XX?XX?X
Maska je složena ze 2 znaků, X a ?:
obsahuje-li maska na dané pozici znak X, odpovídající znak
slova se nemění,
naopak, je-li na dané pozici znak ?, odpovídající znak ve
slově se zamaskuje některým znakem ze seznamu alternatives.
Funkce wordmask pak vrátí seznam všech slov (v libovolném
pořadí), které mohou tímto postupem vzniknout.
Například pro slovo abababa, masku XX? a seznam alternativ
['x', 'y'] bude výsledkem maskování některá permutace seznamu
['abxbaxa', 'abybaxa', 'abxbaya', 'abybaya'].
Napište čistou funkci highly_composite, která dostane na vstupu
množinu přirozených čísel a vrátí množinu těch z nich, která jsou
vysoce složená relativně k původní množině. Přirozené číslo je
vysoce složené, má-li striktně víc dělitelů (a to včetně těch,
které v zadané množině nejsou), než libovolné menší číslo ze
zadané množiny.
V této úloze budeme implementovat simulaci procházky po 2D mřížce.
Pro reprezentaci pozice v mřížce budeme používat uspořádanou
dvojici .
Position = tuple[int, int]
Cesta procházky je zadaná jako řetězec path, který se skládá
z příkazů ← / → pro pohyb doleva a doprava (po ose ) a ↑
/ ↓ pro pohyb nahoru a dolů (po ose ). Souřadnice rostou ve
směru doprava na -ové ose a nahoru na -ové ose.
Napište čistou funkci walk, která vrátí finální pozici pro
procházku path z počáteční pozice start.
Dále napište čistou funkci meet, která vrátí pro dvojici cest
path_1, path_2 a počátků start_1 a start_2, první pozici
na které se procházky potkají. Procházky se provádí synchronně,
tj. kroky se vykonávají najednou pro obě procházky. Pokud se
procházky nepotkají, funkce vrátí None.
V této úloze budeme programovat jednoduše zřetězený seznam, který
si v každém uzlu udržuje seznam hodnot data maximální délky
capacity. Jinak je zřetězený seznam definován tak, jak jej už
znáte:
Napište metodu append, která vloží hodnotu value na konec
posledního uzlu, není-li plný, jinak vytvoří nový uzel na
konci seznamu.
def append(self, value: int) -> None:
pass
Napište metodu delete, která smaže první výskyt hodnoty
value ze seznamu. Pokud by po smazání nastalo, že zůstane
v seznamu prázdny uzel, smaže se i ten. Například mějme
následující seznam:
Po smazání hodnoty 5 bude výsledný seznam vypadat
následovně:
Naproti tomu smazáním hodnoty 3 z původního seznamu vznikne
prázdny uzel, který se smaže:
def delete(self, value: int) -> None:
pass
Konečně napište metodu compact, která maximalizuje využití
kapacity uzlů: přesune prvky v seznamu tak, aby se uzly
v seznamu odpředu zaplnily. Přebytečné prázdné uzly metoda
smaže. Ve výsledném seznamu zachovejte vzájemné pořadí prvků.
Například kompaktní reprezentace pro seznam z předchozího
příkladu a kapacitu 3 je:
† Obecný proud je datová struktura podobná seznamu, která je
potenciálně nekonečná, ale funguje přitom i v programovacích
jazycích se striktním vyhodnocováním. V tomto příkladu se omezíme
na nekonečné cyklické proudy. Do třídy Stream si doplňte
potřebné atributy. Metoda get z proudu vybere další prvek (tzn.
odstraní první prvek a vrátí jej).
class Stream:
def __init__(self, data: list[int]) -> None:
pass
def get(self) -> int:
pass
Čistá funkce cycle ze seznamu (který je konečný) vytvoří proud
(který je nekonečný), a to tak, že pomyslně zřetězí nekonečně
mnoho kopií tohoto seznamu za sebe.
def cycle(data: list[int]) -> Stream:
pass
Čistá funkce drop odstraní ze vstupního proudu n počátečních
prvků a vrátí výsledný proud.
Čistá funkce every_nth vytvoří proud, který vznikne z toho
vstupního tak, že vždy jeden prvek zachová a pak prvků
přeskočí. Jinými slovy, vyberete ze vstupního proudu ty prvky,
které jsou na pozicích dělitelných .
† V tomto příkladě pokračujeme proudy. Tentokrát budou proudy
obecné: mohou být jak konečné tak nekonečné, a nemusí být
cyklické. Protože v obecném případě nelze proud uložit celý,
musíme datovou strukturu naprogramovat tak, aby potřebný výpočet
proběhl až ve chvíli, kdy se pokusíme z proudu vybrat další prvek.
To zabezpečíme tak, že každá transformace proudu bude samostatná
třída, která si bude pamatovat odkaz na vnitřní proud (t.j. ten,
který transformuje) a podle potřeby z něj bude vybírat prvky.
Protože všechny tyto třídy mají metodu take_head, obecný proud
lze reprezentovat jako instanci libovolné z těchto tříd.
Definici typu 'Stream' naleznete níže.
Třída FinStream bude reprezentovat konečný proud, který vznikl
ze seznamu konverzní funkcí to_stream. Ostatní třídy
reprezentují transformace popsané níže u příslušných funkcí.
class FinStream:
def __init__(self, data: list[int]) -> None:
pass
Metoda take_head vrátí dvojici, kde první složka je první
prvek proudu (existuje-li) a druhá složka reprezentuje proud,
který vznikne odstraněním prvního prvku.
Čistá funkce, která vytvoří konečný proud z dat zadaných v seznamu.
def to_stream(data: list[int]) -> Stream:
pass
Čistá funkce, která vytvoří nekonečný proud, a to tak, že bude
vybírat prvky z vnitřního proudu, dokud to lze. V případě, že
prvky dojdou (vstupní proud byl konečný), výstupní proud se vrátí
na začátek toho vstupního a toto bude dále opakovat (libovolně
dlouho).
def cycle(inner: Stream) -> Stream:
pass
Čistá funkce, která vytvoří nový proud tím, že zahodí prvních n
prvků toho vstupního.
Čistá funkce, která vytvoří proud, který se bude chovat
následovně: první prvek vybere z proudu data, pak dalších n
prvků přeskočí, kde n je hodnota vybraná z proudu skips. Toto
bude opakovat, dokud budou v data nějaké prvky. Dojdou-li
v skips hodnoty, výsledný proud nebude dále nic přeskakovat.
S polynomy jsme se už setkali dvakrát, v kapitolách 5 a 7. Ještě
jednou si připomeňme, jak polynomy vypadají:
Tentokrát budeme pracovat s řetězcovou reprezentací polynomů,
která vypadá jako výše uvedený zápis, pouze místo bude
obsahovat konkrétní koeficienty. Pro lepší čitelnost budeme navíc
požadovat, aby byly záporné koeficienty v řetězci zapsané jako
, nikoliv jako . Vaším úkolem je napsat
dvojici funkcí: poly_to_str, která převede seznam koeficientů na
řetězec a str_to_poly která realizuje opačnou konverzi.
Koeficienty budou v seznamech v pořadí na indexu .
Máte připraveny třídy, které budou tvořit AST (abstraktní syntaktický strom)
velmi jednoduchého programu:
Arithmetic reprezentuje binární aritmetickou operaci;
její objekty mají atributy op (jeden z řetězců '+', '-',
'*', '/'), left (levý operand), right (pravý operand).
Assignment reprezentuje přiřazení; její objekty mají atributy
var (řetězec, jméno proměnné na levé straně přiřazení) a rhs
(pravá strana přiřazení).
Dále je připraven typový alias Expression, který reprezentuje
uzel stromu výrazu – buď číslo typu int nebo řetězec (reprezentuje
proměnnou) nebo objekt typu Arithmetic. Výše uvedené atributy
left, right a rhs jsou typu Expression.
Tyto třídy ani typový alias Expression nijak nemodifikujte.
class Arithmetic:
def __init__(self, op: str, left: 'Expression',
right: 'Expression'):
self.op = op
self.left = left
self.right = right
Expression = Arithmetic | str | int
class Assignment:
def __init__(self, var: str, rhs: Expression):
self.var = var
self.rhs = rhs
Napište čistou funkci, která dostane na vstupu jednoduchý program ve formě
seznamu přiřazení a vrátí slovník reprezentující hodnoty proměnných na konci
programu. Pokud během vykonávání programu dojde k chybě (dělení nulou nebo
použití proměnné, které předtím nebyla přiřazena hodnota), funkce vrátí
None. Dělení je vždy celočíselné (i když je reprezentováno znakem /).
Mějme jednoduchý programovací jazyk, jehož (jednoznakové) instrukce
se vyhodnocují nad neomezenou pamětí. Paměť indexujeme celými čísly,
přičemž každá paměťová buňka drží jedno celé číslo; na začátku obsahují
všechny buňky v paměti číslo 0. V průběhu vykonávání programu si
pamatujeme index aktuální buňky; na začátku je to 0.
Instrukce jazyka jsou následující:
'<' – snížíme *index aktuální buňky* o 1;
'>' – zvýšíme *index aktuální buňky* o 1;
'+' – zvýšíme hodnotu aktuální buňky o 1;
'-' – snížíme hodnotu aktuální buňky o 1;
'[' – je-li hodnota aktuální buňky rovna nule,
skočíme za odpovídající znak ']';
']' – skočíme **na** odpovídající znak '['.
O programu předpokládáme, že je vzhledem ke znakům '[' a ']' dobře
uzávorkovaný. Není-li výše řečeno jinak, po provedení instrukce se
přesuneme na instrukci následující. Program končí ve chvíli, kdy by další
provedená instrukce měla ležet za jeho koncem.
Provedení každé jednotlivé instrukce by nemělo trvat příliš dlouho
(ideálně by mělo být skoro konstantní; zejména by nemělo záviset na délce
programu). Je v pořádku si něco předpočítat, než začnete provádět instrukce
programu.
Napište čistou funkci execute, která vyhodnotí zadaný program a vrátí obsah
paměťových buněk jako slovník. Při testování ignorujeme paměťové buňky, které
obsahují hodnotu 0, tedy např. slovníky {1: 0, 2: 3} a {2: 3} jsou
z hlediska testů ekvivalentní.
Tabulkové procesory často pro označení sloupců používají znaky anglické
abecedy, přičemž po vyčerpání 26 možností A až Z se pokračuje
AA, AB, ..., ZZ, AAA, AAB, ...
Čistá funkce spreadsheet_column dostane jako parametr index sloupce
(nezáporné celé číslo, indexujeme od 0) a vrátí řetězec příslušný danému
sloupci. Indexu 2 tedy odpovídá řetězec "C", indexu 27 řetězec "AB",
indexu 16383 řetězec "XFD".
Funkce musí rozumně rychle fungovat pro libovolně velká čísla.
I v této sadě si naprogramujete jednu hru, a bude jí Minesweeper.
Naše verze bude trochu modifikovaná, zejména kliknutí na minu nebude nutně
znamenat konec hry, ale způsobí výbuch, který poškodí část herní plochy.
(Každá mina bude mít přiřazenu tzv. „sílu“ určující, kolik okolních políček
bude zasaženo.)
Abyste si hru mohli vyzkoušet (poté, co implementujete všechny níže
uvedené metody), máte opět k dispozici soubor game_minesweeper.py, který
spusťte ze stejného adresáře, jako je soubor s vaším řešením. Na začátku
souboru jsou konstanty, jejichž úpravou můžete změnit velikost herní
plochy, počet min a vzhled hry.
Třída Minesweeper, kterou máte implementovat, reprezentuje stav hry,
tj. obsah herní plochy, pozici min a aktuální skóre. Interní detaily jsou
na vás, nicméně očekáváme, že objekty této třídy budou mít alespoň tyto
dva atributy:
status – 2D seznam (seznam seznamů – řádků) reprezentující stav hry;
prvky vnitřních seznamů jsou těchto hodnot (UNKNOWN, EXPLODED, DESTROYED
jsou celočíselné konstanty definované níže):
UNKNOWN představuje dosud neodkryté (a nezničené) políčko,
EXPLODED představuje vybuchlou minu,
DESTROYED představuje políčko zničené výbuchem,
0 až 8 představují odkryté políčko s informací o počtu
sousedících min.
score – počet bodů (celé číslo); body se udělují takto:
+1 bod za každé odkrytí políčka bez miny,
-10 bodů za každou vybuchlou minu.
Kliknutí na některé políčko herní plochy bude zpracováno metodou uncover
(viz níže). Je-li již políčko odkryté nebo zničené výbuchem, tato metoda
nemá žádný efekt. V opačném případě se políčko odkryje a nastane jeden
z těchto případů:
Je-li na tomto políčku mina, vybuchne a všechna políčka ve vzdálenosti
menší nebo rovné síle miny budou zničena. Pokud na některém z těchto
políček byla dosud nevybuchlá mina, rovněž vybuchne. To může zničit
další políčka a tento proces se může opakovat (i vícekrát).
Políčka, kde vybuchla mina, se označí stavem EXPLODED, ostatní zničená
políčka se označí stavem DESTROYED. (Stav EXPLODED na herní ploše
zůstává a nemění se na DESTROYED ani při dalším výbuchu.)
V opačném případě se stav políčka nastaví na 0 až 8 podle počtu
min (i těch už vybuchlých) v bezprostředním okolí. Je-li stav 0,
odkryjí se všechna okolní políčka, což se opět může vícekrát opakovat.
Pojmy „okolí“ a „vzdálenost“ zde chápeme ve všech osmi směrech (tedy
i diagonálně). Vybuchlá mina se silou 1 tedy zničí až osm políček,
vybuchlá mina se silou 2 zničí až 24 políček atd.
Souřadnice zde používáme opět ve tvaru (sloupec, řádek), přičemž sloupce
číslujeme od 0 zleva a řádky od 0 shora.
Hodnoty níže uvedených konstant neměňte.
Position = tuple[int, int]
UNKNOWN = -1
EXPLODED = -2
DESTROYED = -3
class Minesweeper:
Po inicializaci mají být všechna pole herní plochy neodkrytá,
herní plocha má mít rozměry zadané parametry width a height
a skóre má být 0. Parametr mines určuje pozici min (klíče slovníku)
a jejich sílu (hodnoty slovníku). Slovník mines nijak nemodifikujte.
Pokud si ho hodláte někam uložit, tak buďto zařiďte, aby se ani později
nemodifikoval, nebo si vytvořte jeho kopii.
Metoda uncover provede odkrytí políčka dle popisu výše a případně
upraví skóre. Předpokládejte, že souřadnice jsou validní (tj. v rozsahu
herní plochy).
Vrátíme se k robotovi, jehož pohyb jsme simulovali ve druhé sadě úkolů
(b_robot). Budeme mít opět stejný plán ve tvaru neomezené čtvercové sítě
s čtvercovými dílky s nákresy ulic či křižovatek. Tentokrát ovšem dáme
robotovi možnost se pohybovat libovolným směrem podle možností na
aktuálním dílku.
Heading = int
NORTH, EAST, SOUTH, WEST = 0, 1, 2, 3
Tile = set[Heading]
Position = tuple[int, int]
Plan = dict[Position, Tile]
Implementujte čistou funkci navigate, která vrátí cestu, kterou se robot
dostane ze zadané startovní do zadané cílové pozice na zadaném plánu.
Pokud žádná taková cesta neexistuje, funkce vrátí None. Vrácená cesta
je ve formě seznamu všech pozic, kterými robot projde od startovní do
cílové pozice včetně. Předpokládejte, že plán je korektní, tj. splňuje
predikát is_correct z úlohy s2/b_robot, a že zadané pozice jsou na
některém z položených dílků.
Doporučení: Použijte princip backtrackingu. Budete muset nějak zařídit, aby
robot neběhal v kruzích (pak by vaše funkce nemusela skončit).
Slovní aritmetika (někdy též cryptarithm nebo algebrogram) je matematický
hlavolam zadaný jako rovnice se slovy, např. „SEND + MORE = MONEY“.
Cílem je přiřadit každému písmenu unikátní20 číslici tak, aby po jejich
nahrazení rovnost platila. Přitom zápis žádného z čísel nesmí začínat nulou.
V tomto konkrétním případě (a v desítkové soustavě) je jediné možné
řešení, a to S → 9, E → 5, N → 6, D → 7, M → 1, O → 0, R → 8, Y → 2.
Po tomto nahrazení číslicemi skutečně platí .
Cílem této úlohy je napsat čistou funkci, která podobné hlavolamy řeší,
a to v zadané poziční soustavě (základem bude vždy celé číslo mezi 2 a 26
včetně). Omezíme se přitom pouze na sčítání, jiné aritmetické operace
neuvažujeme. Rovnice na vstupu je zadána dvěma parametry. Levá strana rovnice
lhs je seznam (alespoň dvou) slov, přičemž každé slovo je dáno jako seznam
písmen (jednoznakových řetězců). Pravá strana rovnice je pak je vždy právě
jedno slovo (seznam písmen).
Funkce vrátí slovník, který každému písmenu hlavolamu přiřazuje unikátní
hodnotu číslice. Pokud existuje více řešení, funkce vrátí libovolné
z nich. Pokud neexistuje žádné řešení, funkce vrátí None.
Nápověda: Použijte techniku backtrackingu. Vzpomeňte si na svá
základoškolská léta – zejména na sčítání pod sebou, které začíná vždy
zprava. I zde se k řešení hodí postupně zkoušet přiřazovat hodnoty
číslicím, které jsou u jednotlivých sčítanců co nejvíce vpravo, a rekurzi
včas ukončit, když už je jasné, že výsledku není možno dosáhnout.
Malované křížovky (nonogramy) jsou logické hlavolamy, u kterých je cílem
vybarvit některá políčka čtvercové sítě podle zadané číselné legendy.
Výsledkem je typicky jednoduchý obrázek. Existují různé druhy malovaných
křížovek, v této úloze nás budou zajímat pouze ty základní černobílé.
Zadání malované křížovky vypadá např. takto:
Číselná legenda u řádků a sloupců ukazuje, kolik políček máme v dané řadě
(řádku nebo sloupci) vybarvit a jak mají být vybarvená políčka seskupena.
Pokud bychom tedy například měli legendu 1 3 2 a řádek délky 9 políček,
pak jej můžeme vyplnit jedním z těchto způsobů:
Řešením malované křížovky je vybarvení políček takové, že každý řádek
a každý sloupec odpovídá zadané legendě. Výše uvedený příklad má tedy
následující (jediné) řešení:
V této úloze si zkusíte napsat program, který bude schopen některé jednodušší
malované křížovky řešit pomocí techniky backtrackingu. Jednotlivá políčka
křížovky budeme reprezentovat typem Pixel, což je zde typový alias pro
int použitý pouze pro lepší čitelnost anotací.
Pixel = int
EMPTY, FULL, UNKNOWN = 0, 1, 2
Máme zde připravené globální konstanty EMPTY (reprezentuje prázdné
políčko), FULL (reprezentuje vybarvené políčko), UNKNOWN (reprezentuje
neznámý stav políčka). Počet různých druhů políček si můžete pro účely
implementace případně rozšířit, ale tyto tři konstanty zachovejte.
Dále máme připraven typový alias pro číselnou legendu. Legenda pro řádky bude
v seznamu uložená zleva doprava, legenda pro sloupce shora dolů.
Clue = list[int]
Nakonec je připravena třída Picture, která bude reprezentovat výsledný
obrázek. Tuto třídu můžete libovolně upravovat (přidávat vlastní atributy
a metody), ale zachovejte parametry metody __init__ i způsob inicializace
atributů height, width a pixels.
class Picture:
def __init__(self, height: int, width: int):
self.height = height
self.width = width
self.pixels = [[UNKNOWN for _ in range(width)]
for _ in range(height)]
Nejprve implementujte čistou funkci gen_lines_with_prefix, která vrátí
seznam všech řad délky size, které odpovídají zadané legendě (clue)
a zároveň začínají zadaným prefixem (prefix). Předpokládejte, že prefix
má délku nejvýše size a obsahuje pouze hodnoty EMPTY a FULL.
Na pořadí seznamů uvnitř vnějšího seznamu nezáleží.
Nápověda: Využijte backtracking. Zkuste začít implementací pro situace,
kdy je prefix prázdný, a tuto implementaci pak rozšiřte.
Dále implementujte čistou funkci solve, která najde řešení malované
křížovky se zadanou legendou. Pokud žádné řešení neexistuje, vrátí None.
Pokud existuje více než jedno řešení, vrátí libovolné z nich.
Nápověda: Využijte backtracking. Použijte funkci gen_lines_with_prefix.
Začněte v levém horním rohu. Střídejte řádky a sloupce. V testech budeme
používat jen takové vstupy, které se tímto přístupem dají dostatečně rychle
vyřešit.
Numberlink je logický hlavolam, v němž je zadána čtvercová síť s několika
dvojicemi čísel a cílem je spojit všechny dvojice stejných čísel lomenou
čarou, přičemž každým políčkem čtvercové sítě musí procházet právě jedna
čára. V naší implementaci místo kreslení čar do čtvercové sítě vepíšeme
čísla všude tam, kudy by spojnice zadaných čísel prošla.
Příklad vstupu:
a řešení:
(Srovnejte s obrázkem na Wikipedii.)
Máme připravené typové aliasy Grid pro 2D seznamy, Position pro dvojice
souřadnic (sloupec, řádek; číslujeme jako obvykle od nuly zleva a shora)
a Ends, jehož význam je vysvětlen níže.
Nejprve implementujte čistou funkci get_ends, která dostane na vstup zadání
hlavolamu jako 2D seznam, který obsahuje pouze nezáporná celá čísla, přičemž
nuly reprezentují prázdná políčka a ostatní čísla ve vstupu jsou vždy přesně
dvakrát. Funkce vrátí slovník typu Ends, v němž jsou pro každé kladné číslo
n ze vstupu dvě položky:
(n, True): (x_1, y_1) a (n, False): (x_2, y_2),
kde (x_1, y_1) a (x_2, y_2) jsou souřadnice výskytu daného čísla.
Na tom, které souřadnice jsou u položky s True a s False, nezáleží.
True, False zde používáme jenom proto, abychom mohli mít dvě různé
položky pro každé číslo.
(Proč volíme zrovna takovou reprezentaci, je vysvětleno níže.)
def get_ends(grid: Grid) -> Ends:
pass
Dále implementujte čistou funkci solve, která najde řešení pro zadaný
vstup. Pokud žádné řešení neexistuje, vrátí None. Pokud existuje více než
jedno řešení, vrátí libovolné z nich.
Nápověda: Využijte backtracking. Spočítejte si nejdříve pozice čísel pomocí
funkce get_ends. Na tyto pozice se můžete dívat jako na dva konce provázku,
které se snažíte dostat k sobě a spojit. V každém kroku backtrackingu si
zvolte jeden z „konců“ a pokuste se jej posunout – možné směry posunutí jsou
právě ty lokální volby, které při backtrackingu provedete. Přitom je vhodné
volit z možných konců takový, který má co nejméně těchto možných směrů.
Kromě posouvání konců si zároveň chcete zaznamenat, která políčka už jsou
obsazena.
def solve(grid: Grid) -> Grid | None:
pass
Poznámka k volbě typu Ends pro reprezentaci „konců provázků“:
Mnozí by jistě mohli navrhnout, že mít ve dvojicích klíčů arbitrární
hodnoty True a False je zbytečné a že by se slovník „konců“ dal
napsat jinak (např. s typem dict[int, tuple[Position, Position]]).
Zde zvolený typ má ale jistou symetrii, která je výhodná pro implementaci
funkce solve. Ke všem „koncům“ se totiž chováme stejně, a tedy kód
pro nalezení jednoho konkrétního (toho s nejméně možnostmi pohybu)
stejně jako kód pro jeho posunutí můžeme napsat obecně a nemusíme u toho
rozebírat více různých případů.
def count_digit_in_sequence(digit, low, high):
count = 0
if low == 0 and digit == 0:
count += 1
for number in range(low, high + 1):
while number > 0:
if digit == number % 10:
count += 1
number = number // 10
return count
def fridays(year, day_of_week):
count = 0
for month in range(1, 13):
days = days_per_month(year, month)
for day in range(1, days + 1):
if is_friday(day_of_week) and day == 13:
count += 1
day_of_week = (day_of_week + 1) % 7
return count
def delete_to_maximal(number):
result = 0
power = 1
while number // power > 0:
candidate = number // (power * 10) * power + number % power
power *= 10
if result < candidate:
result = candidate
return result
def delete_k_to_maximal(number, k):
for i in range(k):
number = delete_to_maximal(number)
return number
def is_parasitic(num: nat1, base: int) -> 'int | None':
orig = num
last = num % base
power = 1
while num >= base:
power *= base
num //= base
new = orig // base + last * power
return new // orig if new % orig == 0 else None
def has_overlap(a, b):
(ax1, ay1), (ax2, ay2) = a
(bx1, by1), (bx2, by2) = b
return ax1 <= bx2 and ax2 >= bx1 and ay1 <= by2 and ay2 >= by1
def filter_overlapping(rectangles):
out = []
count = len(rectangles)
for i in range(count):
for j in range(count):
if i != j and has_overlap(rectangles[i], rectangles[j]):
out.append(rectangles[i])
break
return out
def fridays(year: Year, day_of_week: Day) -> int:
count = 0
for month in range(1, 13):
days = days_per_month(year, month)
for day in range(1, days + 1):
if is_friday(day_of_week) and day == 13:
count += 1
day_of_week = (day_of_week + 1) % 7
return count
def residuals_vectors(x: list[float], y: list[float],
alpha: float, beta: float) -> list[float]:
points = [(x[i], y[i]) for i in range(len(x))]
return residuals_points(points, alpha, beta)
def residuals_points(points: list[tuple[float, float]],
alpha: float, beta: float) -> list[float]:
res = []
for i, (x, y) in enumerate(points):
res.append(abs(y - beta * x - alpha))
return res
Přeskládá a přepočítá prvky pole tak, že nejprve budou
poloviny sudých prvků a poté dvojnásobky lichých prvků.
result = [0] * len(nums)
i = 0
for num in nums:
if num % 2 == 0:
result[i] = num // 2
i += 1
for num in nums:
if num % 2 != 0:
result[i] = num * 2
i += 1
return result
def mysterious_shift(arr: list[float]) -> list[float]:
def is_transitive(relation: set[tuple[int, int]]) -> bool:
for a, b in relation:
for b_prime, c in relation:
if b == b_prime and (a, c) not in relation:
return False
return True
def set_difference(a: set[int], b: set[int]) -> set[int]:
result = set()
for x in a:
if x not in b:
result.add(x)
return result
def set_remove(to_reduce: set[int], other: set[int]) -> None:
for x in other:
if x in to_reduce:
to_reduce.remove(x)
def set_symmetric_diff(a: set[int], b: set[int]) -> set[int]:
result = set()
for x in a:
if x not in b:
result.add(x)
for x in b:
if x not in a:
result.add(x)
return result
def set_symmetric_inplace(to_change: set[int],
other: set[int]) -> None:
for x in other:
if x in to_change:
to_change.remove(x)
else:
to_change.add(x)
def image(f: dict[int, int], values: set[int]) -> set[int]:
result = set()
for x in values:
if x in f:
result.add(f[x])
return result
def preimage(f: dict[int, int], values: set[int]) -> set[int]:
result = set()
for x in f.keys():
if f[x] in values:
result.add(x)
return result
def compose(f: dict[int, int], g: dict[int, int]) -> dict[int, int]:
result = {}
for x in g.keys():
result[x] = f[g[x]]
return result
def kernel(f: dict[int, int]) -> set[tuple[int, int]]:
result = set()
for x in f.keys():
for y in f.keys():
if f[x] == f[y]:
result.add((x, y))
return result
while True:
for num in prev_set:
next_set.update(apply_f_on_num(num))
if len(prev_set) == len(next_set):
return result
result += 1
prev_set.update(next_set)
while row:
next_row = []
for node in row:
for succ in tree[node]:
next_row.append(succ)
if len(next_row) > maximal:
maximal = len(next_row)
row = next_row
while stack:
top = stack[-1]
if top in const:
results[top] = const[top]
elif top in expr:
op, left, right = expr[top]
if left in results and right in results:
results[top] = operation(op, results[left],
results[right])
else:
stack.append(left)
stack.append(right)
continue # do not pop
else:
results[top] = 0
stack.pop()
def all_connected(stops: dict[str, list[str]]) -> bool:
for stop in stops.keys():
stack = [stop]
reachable = {stop}
while stack:
for current in stops[stack.pop()]:
if current not in reachable and current != stop:
reachable.add(current)
stack.append(current)
if stops.keys() != reachable:
return False
return True
def add_warrior(self, clan: str, warrior: Warrior) -> None:
if clan not in self._clans:
self._clans[clan] = [warrior]
else:
self._clans[clan].append(warrior)
def validate_clan_strength(self, required: int) -> bool:
for d, ws in self._clans.items():
total = 0
for w in ws:
total += w.strength
if total <= required:
return False
return True
class Node:
def __init__(self, value: int) -> None:
self.value = value
self.next: Node | None = None
XXX the visited set is 'any' (masqueraded as to_test) because
for reasons unknown, python explodes on set[int] here (though
it works elsewhere)
def to_str(self, visited: to_test) -> str:
out = str(self.value)
if id(self) in visited:
out += " (loop)"
elif self.next is not None:
out += ' → ' + self.next.to_str(visited | {id(self)})
return out
it: Node | None = self.head
prev = None
while it is not None and it.value < value:
prev = it
it = it.next
node.next = it
if prev is not None:
prev.next = node
else:
self.head = node
def get_greatest_in(self, value: int, dist: int) -> int | None:
out = None
it = self.head
while it is not None and it.value < value:
it = it.next
while it is not None and it.value <= value + dist:
out = it.value
it = it.next
return out
def make_tests(list_type: to_test) -> to_test:
def construct_linked(values: list[int]) -> to_test:
result = list_type()
for v in values:
result.insert(v)
return result
def insert(self, value: int) -> None:
new_head = Node(value)
if self.head is None:
self.end = new_head
else:
assert self.end is not None
new_head.next = self.head
self.end.next = new_head
self.head = new_head
def last(self) -> Node | None:
return self.end
def split_by_value(self, value: int) -> 'CircularList':
assert self.head is not None
it = self.head
while it.value != value:
it = it.next
return self.split_by_node(it)
def split_by_node(self, node: Node) -> 'CircularList':
assert self.head is not None
assert self.end is not None
def __eq__(self, other: object) -> bool:
if not isinstance(other, LinkedList):
return NotImplemented
a = self.head
b = other.head
while a or b:
if a is None or b is None:
return False
if a.id != b.id:
return False
if a.value != b.value:
return False
a = a.next
b = b.next
return True
def __repr__(self) -> str:
out = '(head)'
ptr = self.head
while ptr is not None:
out += ' → ' + str(ptr.value) + ptr.idstr()
ptr = ptr.next
return out
def build_linked(nums: list[int]) -> LinkedList:
head = Node(0)
tail = head
for i, v in enumerate(nums):
tail.next = Node(v)
tail = tail.next
tail.id = i % 100
result = LinkedList()
result.head = head.next
return result
def group_by_author(self) -> dict[str, list[Book]]:
result: dict[str, list[Book]] = {}
for book in self._books:
if book.author not in result:
result[book.author] = []
result[book.author].append(book)
return result
def ops_enum(index: int) -> list[str]:
ops: list[str] = []
while index:
index, kind = divmod(index, 4)
if kind == 0:
ops.append('shift_left')
elif kind == 1:
ops.append('shift_right')
elif kind == 2:
ops.append('delete_left')
elif kind == 3:
index, value = divmod(index, 7)
ops.append('insert_left ' + str(value + 1))
ops.extend(['shift_left' for _ in range(5)])
ops.extend(['shift_right' for _ in range(5)])
return ops
Ops = Annotated[list[str], ops_enum]
def run(Z: to_test) -> to_test:
def cursor_after_each_op(ops: Ops) -> list[int]:
zipper = Z(0)
out: list[int] = []
for op in ops:
parts = op.split(' ')
if parts == ['shift_left']:
zipper.shift_left()
elif parts == ['shift_right']:
zipper.shift_right()
elif parts == ['delete_left']:
zipper.delete_left()
else:
cmd, value = parts
assert cmd == 'insert_left'
zipper.insert_left(int(value))
out.append(zipper.cursor())
return out
def selectsort(num_list: list[int]) -> None:
for i in range(len(num_list)):
min_idx = i
for j in range(i + 1, len(num_list)):
if num_list[min_idx] > num_list[j]:
min_idx = j
num_list[i], num_list[min_idx] \
= num_list[min_idx], num_list[i]
result = []
index = 0
for nested in arr:
sublist = []
for _ in range(len(nested)):
sublist.append(flattened[index])
index += 1
result.append(sublist)
return result
if left_idx < heap_end and heap[left_idx] > heap[largest]:
largest = left_idx
if right_idx < heap_end and heap[right_idx] > heap[largest]:
largest = right_idx
if largest == idx:
break
else:
heap[largest], heap[idx] = heap[idx], heap[largest]
idx = largest
max_digits = digit_count(max(to_sort))
res = to_sort
for i in range(max_digits):
res = counting_sort_by_digit(res, i)
return res
def counting_sort_by_digit(to_sort: list[int], curr_digit: int) -> \
list[int]:
bucket_size = [0 for i in range(10)]
bucket_start = [0 for i in range(10)]
bucket_index = [0 for i in range(10)]
res = [0 for i in range(len(to_sort))]
for num in to_sort:
bucket_size[digit(num, curr_digit)] += 1
for i in range(1, len(bucket_size)):
bucket_start[i] = bucket_start[i - 1] + bucket_size[i - 1]
for num in to_sort:
d = digit(num, curr_digit)
res[bucket_start[d] + bucket_index[d]] = num
bucket_index[d] += 1
def is_heap(tree: Tree | None) -> bool:
if tree is None:
return True
if not heap_property_check(tree):
return False
return is_heap(tree.left) and is_heap(tree.right)
def heap_property_check(node: Tree) -> bool:
if node.left is not None and node.left.key > node.key:
return False
if node.right is not None and node.right.key > node.key:
return False
return True
def average_branch_len(tree: Tree | None) -> float:
if tree is None:
return 0
branch_lens = all_branch_lens(tree)
return float(sum(branch_lens)) / len(branch_lens)
def all_branch_lens_rec(tree: Tree,
curr_depth: int, lens: list[int]) -> None:
if tree.left is None and tree.right is None:
lens.append(curr_depth)
return
for child in [tree.left, tree.right]:
if child is not None:
all_branch_lens_rec(child, curr_depth + 1, lens)
def partition2pairs(partition: list[set[int]]) -> set[Pair]:
result = set()
for subset in partition:
for elem1 in subset:
for elem2 in subset:
result.add((elem1, elem2))
return result
def pairs2partition(pairs: set[Pair]) -> list[set[int]]:
partitions: dict[int, set[int]] = {}
for (a, b) in pairs:
partitions[a] = partitions.get(a, {a}) | {b}
all_elements = set(partitions.keys())
result: list[set[int]] = []
for element, partition in partitions.items():
if element not in all_elements:
continue
result.append(partition)
for elem in partition:
all_elements.remove(elem)
def flatten(to_flatten: NestedList, result: list[int]) -> list[int]:
for item in to_flatten:
if isinstance(item, int):
result.append(item)
else:
flatten(item, result)
return result
def fill(nested: NestedList, values: list[int], index: int) -> int:
for i, item in enumerate(nested):
if isinstance(item, int):
nested[i] = values[index]
index += 1
else:
index = fill(item, values, index)
return index
with open(path) as file:
all_words = file.read().split()
word_freq: dict[str, int] = {}
for word in all_words:
word = "".join([char for char in word if char.isalpha()])
word = word.lower()
word_freq[word] = word_freq.get(word, 0) + 1
items = [(-freq, word) for word, freq in word_freq.items()]
result = []
for i, (_, word) in enumerate(sorted(items)):
if i == 3:
break
result.append(word)
return result
def split(orig: str, index: int) -> tuple[str, str]:
left = right = ''
for i in range(index):
left += orig[i]
for i in range(index, len(orig)):
right += orig[i]
return left, right
for digit in digits:
result *= 10
result += table[digit]
return result
def ipv4_restore_rec(digits: str, count: int, current: list[str],
result: set[str]) -> set[str]:
if count == 0:
if digits == "":
result.add(".".join(current))
return result
for i in range(1, len(digits) + 1):
left, right = split(digits, i)
if decode_decimal(left) >= 256:
break
current.append(left)
ipv4_restore_rec(right, count - 1, current, result)
current.pop()
with open(input_file) as file:
text = file.read()
word = ""
with open(output_file, "w") as out:
for char in text:
if char.isalpha():
word += char
else:
out.write(corrected_word(word, dictionary))
word = ""
out.write(char)
out.write(corrected_word(word, dictionary))
def corrected_word(word: str,
dictionary: dict[int, set[str]]) -> str:
words = dictionary.get(len(word), set())
word = word.lower()
if not words or word in words:
return word
return best_correction(word, words)
def read_dictionary(path: str) -> dict[int, set[str]]:
res: dict[int, set[str]] = {}
with gzip.open(path, 'rt') as data:
for word in data:
word = word.strip()
key = len(word)
if key not in res:
res[key] = set()
res[key].add(word)
return res
for lang, lang_freq in lang_freqs.items():
a = vector_angle(file_freq, lang_freq)
if a < min_angle:
min_angle = a
min_lang = lang
return min_lang
def letter_freq_vector(filename: str) -> list[int]:
freqs = [0 for i in range(26)]
indices = enumerate(list("abcdefghijklmnopqrstuvwxyz"))
letters = dict([(letter, idx) for idx, letter in indices])
with open(filename) as file:
text = file.read()
for char in text:
if "a" <= char <= "z" or "A" <= char <= "Z":
freqs[letters[char.lower()]] += 1
return freqs
def lang_vectors(languages: dict[str, dict[str, int]]) \
-> dict[str, list[int]]:
res: dict[str, list[int]] = {}
for language, freqs in languages.items():
res[language] = [y for x, y in freqs.items()]
dot_product = sum([v1[i] * v2[i] for i in range(len(v1))])
len_v1 = sqrt(sum([x ** 2 for x in v1]))
len_v2 = sqrt(sum([x ** 2 for x in v2]))
return acos(dot_product / (len_v1 * len_v2))
def step(direction: str, pos: Position) -> Position:
x, y = pos
dx, dy = DIRS[direction]
return (x + dx, y + dy)
def walk(path: str, pos: Position) -> Position:
for direction in path:
pos = step(direction, pos)
return pos
def meet(path_1: str, path_2: str, pos_1: Position,
pos_2: Position) -> Position | None:
if pos_1 == pos_2:
return pos_1
for i in range(max(len(path_1), len(path_2))):
if i < len(path_1):
pos_1 = step(path_1[i], pos_1)
if i < len(path_2):
pos_2 = step(path_2[i], pos_2)
if pos_1 == pos_2:
return pos_1
return None
def delete(self, value: int) -> None:
node = self.head
prev = None
while node is not None:
if value in node.data:
if len(node.data) == 1:
self.unlink(prev, node)
else:
node.data.pop(node.data.index(value))
return
prev = node
node = node.next
def unlink(self, prev: Node | None, node: Node) -> None:
if prev is None:
self.head = node.next
else:
prev.next = node.next
if node == self.tail:
self.tail = prev
def compact(self) -> None:
node = self.head
while node is not None:
self.move_to(node) # fill current node
node = node.next
def move_to(self, node: Node) -> None:
fit = self.capacity - len(node.data)
if node.next is None or fit == 0:
return
if len(node.next.data) <= fit:
node.data.extend(node.next.data)
self.unlink(node, node.next)
self.move_to(node) # need more data → tail-recurse
else:
trimmed = []
for idx, val in enumerate(node.next.data):
if idx < fit:
node.data.append(val)
else:
trimmed.append(val)
node.next.data = trimmed
def to_digits(n: int) -> list[int]:
if n == 0:
return [0]
out = []
while n > 0:
out.append(n % 10)
n //= 10
return out
def from_digits(digits: list[int]) -> int | None:
if not digits:
return None
out = 0
for d in digits:
out *= 10
out += d
return out
def nearest_disjoint(n: int) -> int | None:
digits = to_digits(n)
available = set(range(10)) - set(digits)
tail_len = len(digits) - 1
if not available:
return None
first = digits[-1]
big_digit = max(available)
small_digit = min(available)
small_nonzero = 0 if available == {0} else min(available - {0})
first_small = [x for x in available if x < first]
first_big = [x for x in available if x > first]
lead_small = [max(first_small)] if first_small else []
lead_big = [min(first_big)] if first_big else [small_nonzero, small_digit]
tail_big = [big_digit for i in range(tail_len)]
tail_small = [small_digit for i in range(tail_len)]
smaller = from_digits(lead_small + tail_big)
bigger = from_digits(lead_big + tail_small)
if smaller is not None and bigger is not None and n - smaller < bigger - n:
return smaller
return bigger
Informační systém tvoří primární „rozhraní“ pro stahování studijních
materiálů, odevzdávání řešení, získání výsledků vyhodnocení a čtení
recenzí. Zároveň slouží jako hlavní komunikační kanál mezi studenty
a učiteli, prostřednictvím diskusního fóra.
Máte-li dotazy k úlohám, organizaci, atp., využijte k jejich
položení prosím vždy přednostně diskusní fórum.21 Ke každé kapitole a
ke každému příkladu ze sady vytvoříme samostatné vlákno, kam patří
dotazy specifické pro tuto kapitolu nebo tento příklad. Pro řešení
obecných organizačních záležitostí a technických problémů jsou
podobně v diskusním fóru nachystaná vlákna.
Než položíte libovolný dotaz, přečtěte si relevantní část dosavadní
diskuse – je možné, že na stejný problém už někdo narazil. Máte-li
ve fóru dotaz, na který se Vám nedostalo do druhého pracovního dne
reakce, připomeňte se prosím tím, že na tento svůj příspěvek
odpovíte.
Máte-li dotaz k výsledku testu, nikdy tento výsledek nevkládejte do
příspěvku (podobně nikdy nevkládejte části řešení příkladu). Učitelé
mají přístup k obsahu Vašich poznámkových bloků, i k Vámi odevzdaným
souborům. Je-li to pro pochopení kontextu ostatními čtenáři potřeba,
odpovídající učitel chybějící informace doplní dle uvážení.
Nebojte se do fóra napsat – když si s něčím nevíte rady a/nebo nemůžete najít v materiálech, rádi Vám pomůžeme nebo Vás nasměrujeme na místo, kde odpověď naleznete.
Kostry naleznete ve studijních materiálech v ISu: Student →
IB111 → Studijní materály → Učební materiály. Každá kapitola
má vlastní složku, pojmenovanou 00 (tento úvod a materiály
k nultému cvičení), 01 (první běžná kapitola), 02, …, 12.
Veškeré soubory stáhnete jednoduše tak, že na složku kliknete pravým
tlačítkem a vyberete možnost Stáhnout jako ZIP. Stažený soubor
rozbalte a můžete řešit.
Vypracované příklady můžete odevzdat do odevzdávárny v ISu:
Student → IB111 → Odevzdávárny. Pro přípravy používejte
odpovídající složky s názvy 01, …, 12. Pro příklady ze sad pak
s1_a_csv, atp. (složky začínající s1 pro první, s2 pro druhou
a s3 pro třetí sadu).
Soubor vložíte výběrem možnosti Soubor – nahrát (první ikonka na
liště nad seznamem souborů). Tímto způsobem můžete najednou nahrát
souborů několik (například všechny přípravy z dané kapitoly). Vždy
se ujistěte, že vkládáte správnou verzi souboru (a že nemáte
v textovém editoru neuložené změny). Pozor! Všechny vložené
soubory se musí jmenovat stejně jako v kostrách, jinak nebudou
rozeznány (IS při vkládání automaticky předřadí Vaše UČO – to je
v pořádku, název souboru po vložení do ISu neměňte) .
O každém odevzdaném souboru (i nerozeznaném) se Vám v poznámkovém
bloku log objeví záznam. Tento záznam i výsledky testu syntaxe by
se měl objevit do několika minut od odevzdání (nemáte-li ani po 15
minutách výsledky, napište prosím do diskusního fóra).
Archiv všech souborů, které jste úspěšně odevzdali, naleznete ve
složce Private ve studijních materiálech (Student → IB111 →
Studijní materiály → Private).
Automatickou zpětnou vazbu k odevzdaným úlohám budete dostávat
prostřednictvím tzv. poznámkových bloků v ISu. Ke každé
odevzdávárně existuje odpovídající poznámkový blok, ve kterém
naleznete aktuální výsledky testů. Pro přípravy bude blok vypadat
přibližně takto:
testing verity of submission from 2025-09-17 22:43 CEST
subtest p1_foo passed [ 1]
subtest p2_bar failed
subtest p3_baz failed
subtest p4_quux passed [ 1]
subtest p5_wibble passed [ 1]
subtest p6_xyzzy failed
{bližší popis chyby}
verity test failed
testing syntax of submission from 2025-09-17 22:43 CEST
subtest p1_foo passed
subtest p2_bar failed
{bližší popis chyby}
subtest p3_baz failed
{bližší popis chyby}
subtest p4_quux passed
subtest p5_wibble passed
subtest p6_xyzzy passed
syntax test failed
testing sanity of submission from 2025-09-17 22:43 CEST
subtest p1_foo passed [ 1]
subtest p2_bar failed
subtest p3_baz failed
subtest p4_quux passed [ 1]
subtest p5_wibble passed [ 1]
subtest p6_xyzzy passed [ 1]
sanity test failed
best submission: 2025-09-17 22:43 CEST worth *7 point(s)
Jednak si všimněte, že každý odstavec má vlastní časové razítko,
které určuje, ke kterému odevzdání daný výstup patří. Tato časová
razítka nemusí být stejná. V hranatých závorkách jsou uvedeny dílčí
body, za hvězdičkou na posledním řádku pak celkový bodový zisk za
tuto kapitolu.
Také si všimněte, že best submission se vztahuje na jedno
konkrétní odevzdání jako celek: v situaci, kdy odstavec „verity“ a
odstavec „sanity“ nemají stejné časové razítko, nemusí být celkový
bodový zisk součtem všech dílčích bodů. O konečném zisku rozhoduje
vždy poslední odevzdání před příslušným termínem (opět jako jeden
celek).22
Výstup pro příklady ze sad je podobný, uvažme například:
testing verity of submission from 2025-10-11 21:14 CEST
subtest foo-small passed
subtest foo-large passed
verity test passed [ 7]
testing syntax of submission from 2025-10-14 23:54 CEST
subtest build passed
syntax test passed
testing sanity of submission from 2025-10-14 23:54 CEST
subtest foo passed
sanity test passed
best submission: 2025-10-11 21:14 CEST worth *7 point(s)
Opět si všimněte, že časová razítka se mohou lišit (a v případě
příkladů ze sady bude k této situaci docházet poměrně často, vždy
tedy nejprve ověřte, ke kterému odevzdání se který odstavec vztahuje
a pak až jej dále interpretujte).
Můžete si tak odevzdáním nefunkčních řešení na poslední chvíli snížit výsledný bodový zisk. Uvažte situaci, kdy máte v pátek 2 body za sanity testy příkladů p1, p2, a 1 bod za verity p1. V sobotu odevzdáte řešení, kde p1 neprochází sanity testem, ale p3 ano a navíc u něj projdou i verity testy. Váš výsledný zisk budou stále pouze 3 body (nikoliv 5, protože v žádném odevzdání nejsou zároveň všechna funkční řešení – p1, p2, p3 sanity + p1, p3 verity). Tento mechanismus Vám ovšem nikdy nesníží výsledný bodový zisk pod již jednou dosaženou hranici „best submission“.
Blok corr obsahuje záznamy o manuálních bodových korekcích (např.
v situaci, kdy byl Váš bodový zisk ovlivněn chybou v testech).
Podobně se zde objeví záznamy o penalizaci za opisování.
Blok log obsahuje záznam o všech odevzdaných souborech, včetně
těch, které nebyly rozeznány. Nedostanete-li po odevzdání příkladu
výsledek testů, ověřte si v tomto poznámkovém bloku, že soubor byl
správně rozeznán.
Blok misc obsahuje záznamy o Vaší aktivitě ve cvičení (netýká se
bodů za vzájemné recenze ani vnitrosemestrální testy). Nemáte-li
před koncem cvičení, ve kterém jste řešili příklad u tabule, záznam
v tomto bloku, připomeňte se svému cvičícímu.
Konečně blok sum obsahuje souhrn bodů, které jste dosud získali, a
které ještě získat můžete. Dostanete-li se do situace, kdy Vám ani
zisk všech zbývajících bodů nebude stačit pro splnění podmínek
předmětu, tento blok Vás o tom bude informovat. Tento blok má navíc
přístupnou statistiku bodů – můžete tak srovnat svůj dosavadní
bodový zisk se svými spolužáky.
Je-li blok sum v rozporu s pravidly uvedenými v tomto dokumentu,
přednost mají pravidla zde uvedená. Podobně mají v případě
nesrovnalosti přednost dílčí poznámkové bloky. Dojde-li k takovéto
neshodě, informujte nás o tom prosím v diskusním fóru. Případná
známka uvedená v poznámkovém bloku sum je podobně pouze
informativní – rozhoduje vždy známka zapsaná v hodnocení předmětu.
Použití serveru aisa pro odevzdávání příkladů je zcela volitelné a
vše potřebné můžete vždy udělat i prostřednictvím ISu. Nevíte-li si
s něčím z níže uvedeného rady, použijte IS.
Na server aisa se přihlásíte programem ssh, který je k dispozici
v prakticky každém moderním operačním systému (v OS Windows skrze
WSL23 – Windows Subsystem for Linux). Konkrétní příkaz (za xlogin
doplňte ten svůj):
$ ssh xlogin@aisa.fi.muni.cz
Program se zeptá na heslo: použijte to fakultní (to stejné, které
používáte k přihlášení na ostatní fakultní počítače, nebo např. ve
fadmin-u nebo fakultním gitlab-u).
Veškeré instrukce, které zde uvádíme pro použití na stroji aisa
platí beze změn také na libovolné školní UNIX-ové pracovní stanici
(tzn. z fakultních počítačů není potřeba se hlásit na stroj aisa,
navíc mají sdílený domovský adresář, takže svoje soubory z tohoto
serveru přímo vidíte, jako by byly uloženy na pracovní stanici).
Stažené soubory pak naleznete ve složce ~/ib111. Je bezpečné tento
příkaz použít i v případě, že ve své kopii již máte rozpracovaná
řešení – systém je při aktualizaci nepřepisuje. Došlo-li ke změně
kostry u příkladu, který máte lokálně modifikovaný, aktualizovanou
kostru naleznete v souboru s dodatečnou příponou .pristine, např.
01/e2_concat.cpp.pristine. V takovém případě si můžete obě verze
srovnat příkazem diff:
$ diff -u e2_concat.cpp e2_concat.cpp.pristine
Případné relevantní změny si pak již lehce přenesete do svého
řešení.
Krom samotného zdrojového balíku Vám příkaz ib111 update stáhne i
veškeré recenze (jak od učitelů, tak od spolužáků). To, že máte
k dispozici nové recenze, uvidíte ve výpisu. Recenze najdete ve
složce ~/ib111/reviews.
Odevzdat vypracované (nebo i rozpracované) řešení můžete ze složky
s relevantními soubory takto:
$ cd ~/ib111/01
$ ib111 submit
Přidáte-li přepínač --wait, příkaz vyčká na vyhodnocení testů fáze
„syntax“ a jakmile je výsledek k dispozici, vypíše obsah příslušného
poznámkového bloku. Chcete-li si ověřit co a kdy jste odevzdali,
můžete použít příkaz
$ ib111 status
nebo se podívat do informačního systému (blíže popsáno v sekci T.1).
Pozor! Odevzdáváte-li stejnou sadu příprav jak v ISu tak
prostřednictvím příkazu ib111, ujistěte se, že odevzdáváte vždy
všechny příklady.
Řešíte-li příklad typu r ve cvičení, bude se Vám pravděpodobně
hodit režim sdílení terminálu s cvičícím (který tak bude moct
promítat Váš zdrojový kód na plátno, případně do něj jednoduše
zasáhnout).
Protože se sdílí pouze terminál, budete se muset spokojit
s negrafickým textovým editorem (doporučujeme použít micro,
případně vim umíte-li ho ovládat). Spojení navážete příkazem:
$ ib111 beamer
Protože příkaz vytvoří nové sezení, nezapomeňte se přesunout do
správné složky příkazem cd ~/ib111/NN.
Tato sekce rozvádí obecné principy zápisu kódu s důrazem na
čitelnost a korektnost. Samozřejmě žádná sada pravidel nemůže
zaručit, že napíšete dobrý (korektní a čitelný) program, o nic více,
než může zaručit, že napíšete dobrou povídku nebo namalujete dobrý
obraz. Přesto ve všech těchto případech pravidla existují a jejich
dodržování má obvykle na výsledek pozitivní dopad.
Každé pravidlo má samozřejmě nějaké výjimky. Tyto jsou ale výjimkami
proto, že nastávají výjimečně. Některá pravidla připouští výjimky
častěji než jiná:
Vůbec nejdůležitější úlohou programátora je rozdělit problém tak,
aby byl schopen každou část správně vyřešit a dílčí výsledky pak
poskládat do korektního celku.
Kód musí být rozdělen do ucelených jednotek (kde jednotkou
rozumíme funkci, typ, modul, atd.) přiměřené velikosti, které
lze studovat a používat nezávisle na sobě.
Jednotky musí být od sebe odděleny jasným rozhraním, které by
mělo být jednodušší a uchopitelnější, než kdybychom použití
jednotky nahradili její definicí.
Každá jednotka by měla mít jeden dobře definovaný účel, který
je zachycený především v jejím pojmenování a případně rozvedený
v komentáři.
Máte-li problém jednotku dobře pojmenovat, může to být známka
toho, že dělá příliš mnoho věcí.
Jednotka by měla realizovat vhodnou abstrakci, tzn. měla by
být obecná – zkuste si představit, že dostanete k řešení
nějaký jiný (ale dostatečně příbuzný) problém: bude Vám tato
konkrétní jednotka k něčemu dobrá, aniž byste ji museli
(výrazně) upravovat?
Má-li jednotka parametr, který fakticky identifikuje místo ve
kterém ji používáte (bez ohledu na to, je-li to z jeho názvu
patrné), je to často známka špatně zvolené abstrakce. Máte-li
parametr, který by bylo lze pojmenovat called_from_bar, je to
jasná známka tohoto problému.
Daný podproblém by měl být vyřešen v programu pouze jednou –
nedaří-li se Vám sjednotit různé varianty stejného nebo velmi
podobného kódu (aniž byste se uchýlili k taktice z bodu F), může
to být známka nesprávně zvolené dekompozice. Zkuste se zamyslet,
není-li možné problém rozložit na podproblémy jinak.
Dobře zvolená jména velmi ulehčují čtení kódu, ale jsou i dobrým
vodítkem při dekompozici a výstavbě abstrakcí.
Všechny entity ve zdrojovém kódu nesou anglická jména.
Angličtina je univerzální jazyk programátorů.
Jméno musí být výstižné a popisné: v místě použití je
obvykle jméno náš hlavní (a často jediný) zdroj informací
o jmenované entitě. Nutnost hledat deklaraci nebo definici
(protože ze jména není jasné, co volaná funkce dělá, nebo jaký
má použitá proměnná význam) čtenáře nesmírně zdržuje.24
Jména lokálního významu mohou být méně informativní: je mnohem
větší šance, že význam jmenované entity si pamatujeme, protože
byla definována před chvílí (např. lokální proměnná v krátké
funkci).
Obecněji, informační obsah jména by měl být přímo úměrný jeho
rozsahu platnosti a nepřímo úměrný frekvenci použití: globální
jméno musí být informativní, protože jeho definice je „daleko“
(takže si ji už nepamatujeme) a zároveň se nepoužívá příliš
často (takže si nepamatujeme ani to, co jsme se dozvěděli, když
jsme ho potkali naposled).
Jméno parametru má dvojí funkci: krom toho, že ho používáme
v těle funkce (kde se z pohledu pojmenování chová podobně jako
lokální proměnná), slouží jako dokumentace funkce jako celku.
Pro parametry volíme popisnější jména, než by zaručovalo jejich
použití ve funkci samotné – mají totiž dodatečný globální
význam.
Některé entity mají ustálené názvy – je rozumné se jich držet,
protože čtenář automaticky rozumí jejich významu, i přes
obvyklou stručnost. Zároveň je potřeba se vyvarovat použití
takovýchto ustálených jmen pro nesouvisející entity. Typickým
příkladem jsou iterační proměnné i a j.
Jména s velkým rozsahem platnosti by měla být také
zapamatovatelná. Je vždy lepší si přímo vzpomenout na jméno
funkce, kterou právě potřebuji, než ho vyhledávat (podobně jako
je lepší znát slovo, než ho jít hledat ve slovníku).
Použitý slovní druh by měl odpovídat druhu entity, kterou
pojmenovává. Proměnné a typy pojmenováváme přednostně
podstatnými jmény, funkce přednostně slovesy.
Rodiny příbuzných nebo souvisejících entit pojmenováváme podle
společného schématu:
table_name, table_size, table_items – nikoliv např.
items_in_table;
list_parser, string_parser, set_parser;
find_min, find_max, erase_max – nikoliv např.
erase_maximum nebo erase_greatest nebo max_remove.
Jména by měla brát do úvahy kontext, ve kterém jsou platná.
Neopakujte typ proměnné v jejím názvu (cars, nikoliv
list_of_cars ani set_of_cars) nemá-li tento typ speciální
význam. Podobně jméno nadřazeného typu nepatří do jmen jeho
metod (třída list by měla mít metodu length, nikoliv
list_length).
Dávejte si pozor na překlepy a pravopisné chyby. Zbytečně
znesnadňují pochopení a (zejména v kombinaci s našeptávačem)
lehce vedou na skutečné chyby způsobené záměnou podobných ale
jinak napsaných jmen. Navíc kód s překlepy v názvech působí
značně neprofesionálně.
Nejde zde pouze o samotný fakt, že je potřeba něco vyhledat. Mohlo by se zdát, že tento problém řeší IDE, které nás umí „poslat“ na příslušnou definici samo. Hlavní zdržení ve skutečnosti spočívá v tom, že musíme přerušit čtení předchozího celku. Na rozdíl od počítače je pro člověka „zanořování“ a zejména pak „vynořování“ na pomyslném zásobníku docela drahou operací.
Udržet si přehled o tom, co se v programu děje, jaké jsou vztahy
mezi různými stavovými proměnnými, co může a co nemůže nastat, je
jedna z nejtěžších částí programování.
Přehledný, logický a co nejvíce lineární sled kroků nám ulehčuje
pochopení algoritmu. Časté, komplikované větvení je naopak těžké
sledovat a odvádí pozornost od pochopení důležitých myšlenek.
Nejde-li myšlenku předat jinak, vysvětlíme ji doprovodným
komentářem. Čím těžší myšlenka, tím větší je potřeba komentovat.
Podobně jako jména entit, komentáře které jsou součástí kódu
píšeme anglicky.25
Případný komentář jednotky kódu by měl vysvětlit především „co“
a „proč“ (tzn. jaký plní tato jednotka účel a za jakých
okolností ji lze použít).
Komentář by také neměl zbytečně duplikovat informace, které jsou
k nalezení v hlavičce nebo jiné „nekomentářové“ části kódu –
jestli máte například potřebu komentovat parametr funkce,
zvažte, jestli by nešlo tento parametr lépe pojmenovat nebo
otypovat.
Komentář by neměl zbytečně duplikovat samotný spustitelný kód
(tzn. neměl by se zdlouhavě zabývat tím „jak“ jednotka vnitřně
pracuje). Zejména jsou nevhodné komentáře typu „zvýšíme
proměnnou i o jedna“ – komentář lze použít k vysvětlení proč
je tato operace potřebná – co daná operace dělá si může kažďý
přečíst v samotném kódu.
Tato sbírka samotná představuje ústupek z tohoto pravidla: smyslem našich komentářů je naučit Vás poměrně těžké a často nové koncepty, a její cirkulace je omezená. Zkušenost z dřívějších let ukazuje, že pro studenty je anglický výklad značnou bariérou pochopení. Přesto se snažte vlastní kód komentovat anglicky – výjimku lze udělat pouze pro rozsáhlejší komentáře, které byste jinak nedokázali srozumitelně formulovat. V praxi je angličtina zcela běžně bezpodmínečně vyžadovaná.